feat(expr): fromJSON fails with Json.NET's message, as GitHub prints it
GitHub prints the JsonReaderException of the runner: '<reason> Path '<path>', line <n>, position <n>.'. FromJSON now returns a *JSONError with that text, ported from JsonTextReader (reasons, JSON path, line and position), and fromJSON() uses it as its error message, blank input included. Checked against the runner on the 1459 inputs of testdata/fromjson_errors.json (gen/runner/fromjson_errors.mjs). Comments, constructors and a closing bracket of the wrong kind are not ported and may differ in reason or position; see PORTING.md.
- commit
- 0e48653951ded4c8c77be05e03ed0045d896e173
- author
- brice · · 2026-10-13 03:34 UTC
- committer
- brice · · 2026-10-18 14:59 UTC
- parent
- 226f9bd
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
9 files · +480 -66
this diff is incomplete: 1 file is too large to show. the totals above count everything; clone the repository to read the rest.
M expr/PORTING.md +10 -3
| @@ -235,9 +235,16 @@ package follows the runner. | |||
| 235 | 235 | are errors, as are `01.5`, `012e1`, `00.5` (a leading zero before `.`, | |
| 236 | 236 | `e` or `E` is only valid for a single `0`); a negative number is decimal | |
| 237 | 237 | (`-010` is -10, `-01.5` is -1.5). An unpaired surrogate escape becomes | |
| 238 | - | U+FFFD. Blank input is a `JsonReaderException` (here: "empty input", kept | |
| 239 | - | as the outer message; the Json.NET message texts are not reproduced, only | |
| 240 | - | that the call fails). Not ported: `/* */` and `//` comments (the runner | |
| 238 | + | U+FFFD. Blank input is a `JsonReaderException` ("Error reading JToken from | |
| 239 | + | JsonReader."). **Error texts**: a failure is a `*JSONError` whose text is | |
| 240 | + | the Json.NET message GitHub prints, `<reason> Path '<path>', line <n>, | |
| 241 | + | position <n>.` (`not json` is `Unexpected character encountered while | |
| 242 | + | parsing value: n. Path '', line 0, position 0.`). Reasons, paths and | |
| 243 | + | positions are ported from `JsonTextReader` and checked against the runner | |
| 244 | + | on the 1 459 rejected inputs of `testdata/fromjson_errors.json` | |
| 245 | + | (`gen/runner/fromjson_errors.mjs`); the 59 that involve comments, | |
| 246 | + | constructors (`new Date(1)`) or a closing bracket of the wrong kind are | |
| 247 | + | not reproduced (the message of those may differ in reason or position). Not ported: `/* */` and `//` comments (the runner | |
| 241 | 248 | reads `/*x*/1` as the string `x`) and `new Date(1)` constructors; both are | |
| 242 | 249 | errors here. A `/` after `null`, `true`, `false`, `NaN`, `Infinity`, | |
| 243 | 250 | `-Infinity` or `undefined`, and a `/` between values inside an array or | |
| @@ -235,9 +235,16 @@ package follows the runner. | |||||
| 235 | are errors, as are `01.5`, `012e1`, `00.5` (a leading zero before `.`, | 235 | are errors, as are `01.5`, `012e1`, `00.5` (a leading zero before `.`, | ||
| 236 | `e` or `E` is only valid for a single `0`); a negative number is decimal | 236 | `e` or `E` is only valid for a single `0`); a negative number is decimal | ||
| 237 | (`-010` is -10, `-01.5` is -1.5). An unpaired surrogate escape becomes | 237 | (`-010` is -10, `-01.5` is -1.5). An unpaired surrogate escape becomes | ||
| 238 | - | U+FFFD. Blank input is a `JsonReaderException` (here: "empty input", kept | 238 | + | U+FFFD. Blank input is a `JsonReaderException` ("Error reading JToken from |
| 239 | - | as the outer message; the Json.NET message texts are not reproduced, only | 239 | + | JsonReader."). **Error texts**: a failure is a `*JSONError` whose text is |
| 240 | - | that the call fails). Not ported: `/* */` and `//` comments (the runner | 240 | + | the Json.NET message GitHub prints, `<reason> Path '<path>', line <n>, |
| 241 | + | position <n>.` (`not json` is `Unexpected character encountered while | |||
| 242 | + | parsing value: n. Path '', line 0, position 0.`). Reasons, paths and | |||
| 243 | + | positions are ported from `JsonTextReader` and checked against the runner | |||
| 244 | + | on the 1 459 rejected inputs of `testdata/fromjson_errors.json` | |||
| 245 | + | (`gen/runner/fromjson_errors.mjs`); the 59 that involve comments, | |||
| 246 | + | constructors (`new Date(1)`) or a closing bracket of the wrong kind are | |||
| 247 | + | not reproduced (the message of those may differ in reason or position). Not ported: `/* */` and `//` comments (the runner | |||
| 241 | reads `/*x*/1` as the string `x`) and `new Date(1)` constructors; both are | 248 | reads `/*x*/1` as the string `x`) and `new Date(1)` constructors; both are | ||
| 242 | errors here. A `/` after `null`, `true`, `false`, `NaN`, `Infinity`, | 249 | errors here. A `/` after `null`, `true`, `false`, `NaN`, `Infinity`, | ||
| 243 | `-Infinity` or `undefined`, and a `/` between values inside an array or | 250 | `-Infinity` or `undefined`, and a `/` between values inside an array or | ||
M expr/eval_test.go +10 -10
| @@ -226,11 +226,11 @@ func TestEvalLogicalReturnsOperands(t *testing.T) { | |||
| 226 | 226 | // Short-circuit: the failing call is never evaluated. | |
| 227 | 227 | {"true || fromJSON('')", "Boolean:true"}, | |
| 228 | 228 | {"false && fromJSON('')", "Boolean:false"}, | |
| 229 | - | {"false || fromJSON('')", "ERR empty input"}, | |
| 230 | - | {"true && fromJSON('')", "ERR empty input"}, | |
| 229 | + | {"false || fromJSON('')", "ERR Error reading JToken from JsonReader. Path '', line 0, position 0."}, | |
| 230 | + | {"true && fromJSON('')", "ERR Error reading JToken from JsonReader. Path '', line 0, position 0."}, | |
| 231 | 231 | // Comparisons evaluate both sides. | |
| 232 | - | {"false == fromJSON('')", "ERR empty input"}, | |
| 233 | - | {"fromJSON('') == false", "ERR empty input"}, | |
| 232 | + | {"false == fromJSON('')", "ERR Error reading JToken from JsonReader. Path '', line 0, position 0."}, | |
| 233 | + | {"fromJSON('') == false", "ERR Error reading JToken from JsonReader. Path '', line 0, position 0."}, | |
| 234 | 234 | // Precedence: && binds tighter than ||, ! tighter than comparison. | |
| 235 | 235 | {"true || false && false", "Boolean:true"}, | |
| 236 | 236 | {"(true || false) && false", "Boolean:false"}, | |
| @@ -422,11 +422,11 @@ func TestEvalBuiltinFunctions(t *testing.T) { | |||
| 422 | 422 | {"fromJSON(toJSON(github.event.commits[0])).author.name", "String:Ann"}, | |
| 423 | 423 | ||
| 424 | 424 | // fromJSON | |
| 425 | - | {"fromJSON('')", "ERR empty input"}, | |
| 426 | - | {"fromJSON(' ')", "ERR empty input"}, | |
| 427 | - | {"fromJSON('\n')", "ERR empty input"}, | |
| 428 | - | {"fromJSON(null)", "ERR empty input"}, | |
| 429 | - | {"fromJSON('{')", "ERR Error parsing JSON when evaluating fromJson"}, | |
| 425 | + | {"fromJSON('')", "ERR Error reading JToken from JsonReader. Path '', line 0, position 0."}, | |
| 426 | + | {"fromJSON(' ')", "ERR Error reading JToken from JsonReader. Path '', line 1, position 2."}, | |
| 427 | + | {"fromJSON('\n')", "ERR Error reading JToken from JsonReader. Path '', line 0, position 0."}, | |
| 428 | + | {"fromJSON(null)", "ERR Error reading JToken from JsonReader. Path '', line 0, position 0."}, | |
| 429 | + | {"fromJSON('{')", "ERR Error reading JObject from JsonReader. Path '', line 1, position 1."}, | |
| 430 | 430 | // Json.NET is lenient: trailing commas, unquoted names, leading zeros, | |
| 431 | 431 | // NaN and Infinity and anything after the first value are accepted. | |
| 432 | 432 | {"fromJSON('[1,]')", "Array:Array"}, | |
| @@ -441,7 +441,7 @@ func TestEvalBuiltinFunctions(t *testing.T) { | |||
| 441 | 441 | {"fromJSON('\"\\ud83d\\ude00\"')", "String:😀"}, | |
| 442 | 442 | {"fromJSON(1)", "Number:1"}, | |
| 443 | 443 | {"fromJSON(true)", "Boolean:true"}, | |
| 444 | - | {"fromJSON(matrix.node)", "ERR Error parsing JSON when evaluating fromJson"}, // the string "Array" | |
| 444 | + | {"fromJSON(matrix.node)", "ERR Unexpected character encountered while parsing value: A. Path '', line 0, position 0."}, // the string "Array" | |
| 445 | 445 | {"fromJSON(1, 2)", "PARSE Too many parameters supplied: '('"}, | |
| 446 | 446 | ||
| 447 | 447 | // format | |
| @@ -226,11 +226,11 @@ func TestEvalLogicalReturnsOperands(t *testing.T) { | |||||
| 226 | // Short-circuit: the failing call is never evaluated. | 226 | // Short-circuit: the failing call is never evaluated. | ||
| 227 | {"true || fromJSON('')", "Boolean:true"}, | 227 | {"true || fromJSON('')", "Boolean:true"}, | ||
| 228 | {"false && fromJSON('')", "Boolean:false"}, | 228 | {"false && fromJSON('')", "Boolean:false"}, | ||
| 229 | - | {"false || fromJSON('')", "ERR empty input"}, | 229 | + | {"false || fromJSON('')", "ERR Error reading JToken from JsonReader. Path '', line 0, position 0."}, |
| 230 | - | {"true && fromJSON('')", "ERR empty input"}, | 230 | + | {"true && fromJSON('')", "ERR Error reading JToken from JsonReader. Path '', line 0, position 0."}, |
| 231 | // Comparisons evaluate both sides. | 231 | // Comparisons evaluate both sides. | ||
| 232 | - | {"false == fromJSON('')", "ERR empty input"}, | 232 | + | {"false == fromJSON('')", "ERR Error reading JToken from JsonReader. Path '', line 0, position 0."}, |
| 233 | - | {"fromJSON('') == false", "ERR empty input"}, | 233 | + | {"fromJSON('') == false", "ERR Error reading JToken from JsonReader. Path '', line 0, position 0."}, |
| 234 | // Precedence: && binds tighter than ||, ! tighter than comparison. | 234 | // Precedence: && binds tighter than ||, ! tighter than comparison. | ||
| 235 | {"true || false && false", "Boolean:true"}, | 235 | {"true || false && false", "Boolean:true"}, | ||
| 236 | {"(true || false) && false", "Boolean:false"}, | 236 | {"(true || false) && false", "Boolean:false"}, | ||
| @@ -422,11 +422,11 @@ func TestEvalBuiltinFunctions(t *testing.T) { | |||||
| 422 | {"fromJSON(toJSON(github.event.commits[0])).author.name", "String:Ann"}, | 422 | {"fromJSON(toJSON(github.event.commits[0])).author.name", "String:Ann"}, | ||
| 423 | 423 | ||||
| 424 | // fromJSON | 424 | // fromJSON | ||
| 425 | - | {"fromJSON('')", "ERR empty input"}, | 425 | + | {"fromJSON('')", "ERR Error reading JToken from JsonReader. Path '', line 0, position 0."}, |
| 426 | - | {"fromJSON(' ')", "ERR empty input"}, | 426 | + | {"fromJSON(' ')", "ERR Error reading JToken from JsonReader. Path '', line 1, position 2."}, |
| 427 | - | {"fromJSON('\n')", "ERR empty input"}, | 427 | + | {"fromJSON('\n')", "ERR Error reading JToken from JsonReader. Path '', line 0, position 0."}, |
| 428 | - | {"fromJSON(null)", "ERR empty input"}, | 428 | + | {"fromJSON(null)", "ERR Error reading JToken from JsonReader. Path '', line 0, position 0."}, |
| 429 | - | {"fromJSON('{')", "ERR Error parsing JSON when evaluating fromJson"}, | 429 | + | {"fromJSON('{')", "ERR Error reading JObject from JsonReader. Path '', line 1, position 1."}, |
| 430 | // Json.NET is lenient: trailing commas, unquoted names, leading zeros, | 430 | // Json.NET is lenient: trailing commas, unquoted names, leading zeros, | ||
| 431 | // NaN and Infinity and anything after the first value are accepted. | 431 | // NaN and Infinity and anything after the first value are accepted. | ||
| 432 | {"fromJSON('[1,]')", "Array:Array"}, | 432 | {"fromJSON('[1,]')", "Array:Array"}, | ||
| @@ -441,7 +441,7 @@ func TestEvalBuiltinFunctions(t *testing.T) { | |||||
| 441 | {"fromJSON('\"\\ud83d\\ude00\"')", "String:😀"}, | 441 | {"fromJSON('\"\\ud83d\\ude00\"')", "String:😀"}, | ||
| 442 | {"fromJSON(1)", "Number:1"}, | 442 | {"fromJSON(1)", "Number:1"}, | ||
| 443 | {"fromJSON(true)", "Boolean:true"}, | 443 | {"fromJSON(true)", "Boolean:true"}, | ||
| 444 | - | {"fromJSON(matrix.node)", "ERR Error parsing JSON when evaluating fromJson"}, // the string "Array" | 444 | + | {"fromJSON(matrix.node)", "ERR Unexpected character encountered while parsing value: A. Path '', line 0, position 0."}, // the string "Array" |
| 445 | {"fromJSON(1, 2)", "PARSE Too many parameters supplied: '('"}, | 445 | {"fromJSON(1, 2)", "PARSE Too many parameters supplied: '('"}, | ||
| 446 | 446 | ||||
| 447 | // format | 447 | // format | ||
M expr/example_test.go +1 -1
| @@ -71,7 +71,7 @@ func ExampleEvaluate() { | |||
| 71 | 71 | // join(github.labels.*.name, ', ') -> Bug, docs (truthy: true) | |
| 72 | 72 | // format('{0} has {1} labels', github.ref, 2) -> refs/heads/main has 2 labels (truthy: true) | |
| 73 | 73 | // always() && github.missing -> (truthy: false) | |
| 74 | - | // fromJSON('{') -> error: Error parsing JSON when evaluating fromJson | |
| 74 | + | // fromJSON('{') -> error: Error reading JObject from JsonReader. Path '', line 1, position 1. | |
| 75 | 75 | } | |
| 76 | 76 | ||
| 77 | 77 | func ExampleToJSON() { | |
| @@ -71,7 +71,7 @@ func ExampleEvaluate() { | |||||
| 71 | // join(github.labels.*.name, ', ') -> Bug, docs (truthy: true) | 71 | // join(github.labels.*.name, ', ') -> Bug, docs (truthy: true) | ||
| 72 | // format('{0} has {1} labels', github.ref, 2) -> refs/heads/main has 2 labels (truthy: true) | 72 | // format('{0} has {1} labels', github.ref, 2) -> refs/heads/main has 2 labels (truthy: true) | ||
| 73 | // always() && github.missing -> (truthy: false) | 73 | // always() && github.missing -> (truthy: false) | ||
| 74 | - | // fromJSON('{') -> error: Error parsing JSON when evaluating fromJson | 74 | + | // fromJSON('{') -> error: Error reading JObject from JsonReader. Path '', line 1, position 1. |
| 75 | } | 75 | } | ||
| 76 | 76 | ||||
| 77 | func ExampleToJSON() { | 77 | func ExampleToJSON() { | ||
A expr/fromjson_errors_test.go +101 -0
| @@ -0,0 +1,101 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | package expr | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "context" | |
| 7 | + | "encoding/json" | |
| 8 | + | "errors" | |
| 9 | + | "os" | |
| 10 | + | "strings" | |
| 11 | + | "testing" | |
| 12 | + | ) | |
| 13 | + | ||
| 14 | + | // unportedMessage reports whether the runner's message is about something this | |
| 15 | + | // package does not read: comments, constructors ("new Date(1)"), and the | |
| 16 | + | // stray-bracket check of JContainer. | |
| 17 | + | func unportedMessage(in, msg string) bool { | |
| 18 | + | return strings.Contains(in, "/") || strings.Contains(in, "new ") || | |
| 19 | + | strings.HasPrefix(msg, "Unexpected content while parsing JSON") || strings.Contains(msg, "constructor") || | |
| 20 | + | strings.HasPrefix(msg, "JsonToken ") && !strings.Contains(msg, "closing JsonType None") | |
| 21 | + | } | |
| 22 | + | ||
| 23 | + | // TestFromJSONErrorsMatchRunner replays testdata/fromjson_errors.json, the | |
| 24 | + | // messages of the runner's JsonReaderException for the inputs fromJSON rejects | |
| 25 | + | // (gen/runner/fromjson_errors.mjs). GitHub prints them in the job log. | |
| 26 | + | func TestFromJSONErrorsMatchRunner(t *testing.T) { | |
| 27 | + | data, err := os.ReadFile("testdata/fromjson_errors.json") | |
| 28 | + | if err != nil { | |
| 29 | + | t.Fatal(err) | |
| 30 | + | } | |
| 31 | + | var cs []struct{ In, Msg string } | |
| 32 | + | if err := json.Unmarshal(data, &cs); err != nil { | |
| 33 | + | t.Fatal(err) | |
| 34 | + | } | |
| 35 | + | bad, skipped := 0, 0 | |
| 36 | + | for _, c := range cs { | |
| 37 | + | _, err := FromJSON([]byte(c.In)) | |
| 38 | + | if err != nil && err.Error() == c.Msg { | |
| 39 | + | continue | |
| 40 | + | } | |
| 41 | + | if unportedMessage(c.In, c.Msg) { | |
| 42 | + | skipped++ | |
| 43 | + | continue | |
| 44 | + | } | |
| 45 | + | bad++ | |
| 46 | + | if bad <= 25 { | |
| 47 | + | t.Errorf("FromJSON(%q): %v\n runner: %s", c.In, err, c.Msg) | |
| 48 | + | } | |
| 49 | + | } | |
| 50 | + | t.Logf("%d cases, %d unported (comments, constructors), %d mismatches", len(cs), skipped, bad) | |
| 51 | + | if skipped > len(cs)/10 { | |
| 52 | + | t.Errorf("too many cases skipped: %d", skipped) | |
| 53 | + | } | |
| 54 | + | } | |
| 55 | + | ||
| 56 | + | // The forms of the conformance corpus (expr-runtime-errors): fromJSON of | |
| 57 | + | // "not json", the same text the runner fails on. | |
| 58 | + | func TestFromJSONErrorText(t *testing.T) { | |
| 59 | + | for _, tc := range []struct{ in, want string }{ | |
| 60 | + | {"not json", "Unexpected character encountered while parsing value: n. Path '', line 0, position 0."}, | |
| 61 | + | {"", "Error reading JToken from JsonReader. Path '', line 0, position 0."}, | |
| 62 | + | {" \n ", "Error reading JToken from JsonReader. Path '', line 2, position 1."}, | |
| 63 | + | {"[1,x]", "Unexpected character encountered while parsing value: x. Path '[0]', line 1, position 3."}, | |
| 64 | + | {`{"a":{"b":x}}`, "Unexpected character encountered while parsing value: x. Path 'a.b', line 1, position 10."}, | |
| 65 | + | {"{", "Error reading JObject from JsonReader. Path '', line 1, position 1."}, | |
| 66 | + | {"[1,2", "Unexpected end of content while loading JArray. Path '[1]', line 1, position 4."}, | |
| 67 | + | {`"abc`, "Unterminated string. Expected delimiter: \". Path '', line 1, position 4."}, | |
| 68 | + | {"tru", "Unexpected end when reading JSON. Path '', line 1, position 3."}, | |
| 69 | + | {"[nul]", "Error parsing null value. Path '', line 1, position 4."}, | |
| 70 | + | {"08", "Input string '08' is not a valid number. Path '', line 1, position 2."}, | |
| 71 | + | {"[1\n,\n x]", "Unexpected character encountered while parsing value: x. Path '[0]', line 3, position 1."}, | |
| 72 | + | } { | |
| 73 | + | _, err := FromJSON([]byte(tc.in)) | |
| 74 | + | if err == nil || err.Error() != tc.want { | |
| 75 | + | t.Errorf("FromJSON(%q) = %v, want %s", tc.in, err, tc.want) | |
| 76 | + | } | |
| 77 | + | var je *JSONError | |
| 78 | + | if !errors.As(err, &je) || je.ExceptionClass() != "Newtonsoft.Json.JsonReaderException" { | |
| 79 | + | t.Errorf("FromJSON(%q): not a *JSONError: %T", tc.in, err) | |
| 80 | + | } | |
| 81 | + | } | |
| 82 | + | } | |
| 83 | + | ||
| 84 | + | // fromJSON() fails with the reader's message, and the error unwraps to it. | |
| 85 | + | func TestFromJSONFunctionError(t *testing.T) { | |
| 86 | + | node, err := Parse("fromJSON(env.x)", []string{"env"}, nil) | |
| 87 | + | if err != nil { | |
| 88 | + | t.Fatal(err) | |
| 89 | + | } | |
| 90 | + | _, err = Evaluate(context.Background(), node, EvalContext{Contexts: map[string]Value{"env": func() Value { | |
| 91 | + | d := NewDictionary() | |
| 92 | + | d.Add("x", String("not json")) | |
| 93 | + | return d | |
| 94 | + | }()}}) | |
| 95 | + | want := "Unexpected character encountered while parsing value: n. Path '', line 0, position 0." | |
| 96 | + | var ee *EvalError | |
| 97 | + | var je *JSONError | |
| 98 | + | if !errors.As(err, &ee) || ee.Message != want || !errors.As(err, &je) { | |
| 99 | + | t.Fatalf("%v", err) | |
| 100 | + | } | |
| 101 | + | } | |
| @@ -0,0 +1,101 @@ | |||||
| 1 | + | // SPDX-License-Identifier: MIT | |||
| 2 | + | ||||
| 3 | + | package expr | |||
| 4 | + | ||||
| 5 | + | import ( | |||
| 6 | + | "context" | |||
| 7 | + | "encoding/json" | |||
| 8 | + | "errors" | |||
| 9 | + | "os" | |||
| 10 | + | "strings" | |||
| 11 | + | "testing" | |||
| 12 | + | ) | |||
| 13 | + | ||||
| 14 | + | // unportedMessage reports whether the runner's message is about something this | |||
| 15 | + | // package does not read: comments, constructors ("new Date(1)"), and the | |||
| 16 | + | // stray-bracket check of JContainer. | |||
| 17 | + | func unportedMessage(in, msg string) bool { | |||
| 18 | + | return strings.Contains(in, "/") || strings.Contains(in, "new ") || | |||
| 19 | + | strings.HasPrefix(msg, "Unexpected content while parsing JSON") || strings.Contains(msg, "constructor") || | |||
| 20 | + | strings.HasPrefix(msg, "JsonToken ") && !strings.Contains(msg, "closing JsonType None") | |||
| 21 | + | } | |||
| 22 | + | ||||
| 23 | + | // TestFromJSONErrorsMatchRunner replays testdata/fromjson_errors.json, the | |||
| 24 | + | // messages of the runner's JsonReaderException for the inputs fromJSON rejects | |||
| 25 | + | // (gen/runner/fromjson_errors.mjs). GitHub prints them in the job log. | |||
| 26 | + | func TestFromJSONErrorsMatchRunner(t *testing.T) { | |||
| 27 | + | data, err := os.ReadFile("testdata/fromjson_errors.json") | |||
| 28 | + | if err != nil { | |||
| 29 | + | t.Fatal(err) | |||
| 30 | + | } | |||
| 31 | + | var cs []struct{ In, Msg string } | |||
| 32 | + | if err := json.Unmarshal(data, &cs); err != nil { | |||
| 33 | + | t.Fatal(err) | |||
| 34 | + | } | |||
| 35 | + | bad, skipped := 0, 0 | |||
| 36 | + | for _, c := range cs { | |||
| 37 | + | _, err := FromJSON([]byte(c.In)) | |||
| 38 | + | if err != nil && err.Error() == c.Msg { | |||
| 39 | + | continue | |||
| 40 | + | } | |||
| 41 | + | if unportedMessage(c.In, c.Msg) { | |||
| 42 | + | skipped++ | |||
| 43 | + | continue | |||
| 44 | + | } | |||
| 45 | + | bad++ | |||
| 46 | + | if bad <= 25 { | |||
| 47 | + | t.Errorf("FromJSON(%q): %v\n runner: %s", c.In, err, c.Msg) | |||
| 48 | + | } | |||
| 49 | + | } | |||
| 50 | + | t.Logf("%d cases, %d unported (comments, constructors), %d mismatches", len(cs), skipped, bad) | |||
| 51 | + | if skipped > len(cs)/10 { | |||
| 52 | + | t.Errorf("too many cases skipped: %d", skipped) | |||
| 53 | + | } | |||
| 54 | + | } | |||
| 55 | + | ||||
| 56 | + | // The forms of the conformance corpus (expr-runtime-errors): fromJSON of | |||
| 57 | + | // "not json", the same text the runner fails on. | |||
| 58 | + | func TestFromJSONErrorText(t *testing.T) { | |||
| 59 | + | for _, tc := range []struct{ in, want string }{ | |||
| 60 | + | {"not json", "Unexpected character encountered while parsing value: n. Path '', line 0, position 0."}, | |||
| 61 | + | {"", "Error reading JToken from JsonReader. Path '', line 0, position 0."}, | |||
| 62 | + | {" \n ", "Error reading JToken from JsonReader. Path '', line 2, position 1."}, | |||
| 63 | + | {"[1,x]", "Unexpected character encountered while parsing value: x. Path '[0]', line 1, position 3."}, | |||
| 64 | + | {`{"a":{"b":x}}`, "Unexpected character encountered while parsing value: x. Path 'a.b', line 1, position 10."}, | |||
| 65 | + | {"{", "Error reading JObject from JsonReader. Path '', line 1, position 1."}, | |||
| 66 | + | {"[1,2", "Unexpected end of content while loading JArray. Path '[1]', line 1, position 4."}, | |||
| 67 | + | {`"abc`, "Unterminated string. Expected delimiter: \". Path '', line 1, position 4."}, | |||
| 68 | + | {"tru", "Unexpected end when reading JSON. Path '', line 1, position 3."}, | |||
| 69 | + | {"[nul]", "Error parsing null value. Path '', line 1, position 4."}, | |||
| 70 | + | {"08", "Input string '08' is not a valid number. Path '', line 1, position 2."}, | |||
| 71 | + | {"[1\n,\n x]", "Unexpected character encountered while parsing value: x. Path '[0]', line 3, position 1."}, | |||
| 72 | + | } { | |||
| 73 | + | _, err := FromJSON([]byte(tc.in)) | |||
| 74 | + | if err == nil || err.Error() != tc.want { | |||
| 75 | + | t.Errorf("FromJSON(%q) = %v, want %s", tc.in, err, tc.want) | |||
| 76 | + | } | |||
| 77 | + | var je *JSONError | |||
| 78 | + | if !errors.As(err, &je) || je.ExceptionClass() != "Newtonsoft.Json.JsonReaderException" { | |||
| 79 | + | t.Errorf("FromJSON(%q): not a *JSONError: %T", tc.in, err) | |||
| 80 | + | } | |||
| 81 | + | } | |||
| 82 | + | } | |||
| 83 | + | ||||
| 84 | + | // fromJSON() fails with the reader's message, and the error unwraps to it. | |||
| 85 | + | func TestFromJSONFunctionError(t *testing.T) { | |||
| 86 | + | node, err := Parse("fromJSON(env.x)", []string{"env"}, nil) | |||
| 87 | + | if err != nil { | |||
| 88 | + | t.Fatal(err) | |||
| 89 | + | } | |||
| 90 | + | _, err = Evaluate(context.Background(), node, EvalContext{Contexts: map[string]Value{"env": func() Value { | |||
| 91 | + | d := NewDictionary() | |||
| 92 | + | d.Add("x", String("not json")) | |||
| 93 | + | return d | |||
| 94 | + | }()}}) | |||
| 95 | + | want := "Unexpected character encountered while parsing value: n. Path '', line 0, position 0." | |||
| 96 | + | var ee *EvalError | |||
| 97 | + | var je *JSONError | |||
| 98 | + | if !errors.As(err, &ee) || ee.Message != want || !errors.As(err, &je) { | |||
| 99 | + | t.Fatalf("%v", err) | |||
| 100 | + | } | |||
| 101 | + | } | |||
M expr/funcs.go +5 -9
| @@ -195,21 +195,17 @@ func funcToJSON(e *evaluator, c *FunctionCall) (Value, error) { | |||
| 195 | 195 | return String(out), nil | |
| 196 | 196 | } | |
| 197 | 197 | ||
| 198 | - | // funcFromJSON is fromJSON(value). Blank input is "empty input"; anything | |
| 199 | - | // that is not readable by Json.NET is "Error parsing JSON when evaluating | |
| 200 | - | // fromJson" wrapping the reader's error. | |
| 198 | + | // funcFromJSON is fromJSON(value). Anything that Json.NET cannot read, blank | |
| 199 | + | // input included, fails with the message of its JsonReaderException (a | |
| 200 | + | // [*JSONError]), which is what the runner reports. | |
| 201 | 201 | func funcFromJSON(e *evaluator, c *FunctionCall) (Value, error) { | |
| 202 | 202 | a, err := e.eval(c.Args[0]) | |
| 203 | 203 | if err != nil { | |
| 204 | 204 | return nil, err | |
| 205 | 205 | } | |
| 206 | - | in := CoerceString(a) | |
| 207 | - | if strings.TrimFunc(in, isNetSpace) == "" { | |
| 208 | - | return nil, evalErrorf("empty input") | |
| 209 | - | } | |
| 210 | - | v, err := FromJSON([]byte(in)) | |
| 206 | + | v, err := FromJSON([]byte(CoerceString(a))) | |
| 211 | 207 | if err != nil { | |
| 212 | - | return nil, &EvalError{Message: "Error parsing JSON when evaluating fromJson", Err: err} | |
| 208 | + | return nil, &EvalError{Message: err.Error(), Err: err} | |
| 213 | 209 | } | |
| 214 | 210 | return v, nil | |
| 215 | 211 | } | |
| @@ -195,21 +195,17 @@ func funcToJSON(e *evaluator, c *FunctionCall) (Value, error) { | |||||
| 195 | return String(out), nil | 195 | return String(out), nil | ||
| 196 | } | 196 | } | ||
| 197 | 197 | ||||
| 198 | - | // funcFromJSON is fromJSON(value). Blank input is "empty input"; anything | 198 | + | // funcFromJSON is fromJSON(value). Anything that Json.NET cannot read, blank |
| 199 | - | // that is not readable by Json.NET is "Error parsing JSON when evaluating | 199 | + | // input included, fails with the message of its JsonReaderException (a |
| 200 | - | // fromJson" wrapping the reader's error. | 200 | + | // [*JSONError]), which is what the runner reports. |
| 201 | func funcFromJSON(e *evaluator, c *FunctionCall) (Value, error) { | 201 | func funcFromJSON(e *evaluator, c *FunctionCall) (Value, error) { | ||
| 202 | a, err := e.eval(c.Args[0]) | 202 | a, err := e.eval(c.Args[0]) | ||
| 203 | if err != nil { | 203 | if err != nil { | ||
| 204 | return nil, err | 204 | return nil, err | ||
| 205 | } | 205 | } | ||
| 206 | - | in := CoerceString(a) | 206 | + | v, err := FromJSON([]byte(CoerceString(a))) |
| 207 | - | if strings.TrimFunc(in, isNetSpace) == "" { | |||
| 208 | - | return nil, evalErrorf("empty input") | |||
| 209 | - | } | |||
| 210 | - | v, err := FromJSON([]byte(in)) | |||
| 211 | if err != nil { | 207 | if err != nil { | ||
| 212 | - | return nil, &EvalError{Message: "Error parsing JSON when evaluating fromJson", Err: err} | 208 | + | return nil, &EvalError{Message: err.Error(), Err: err} |
| 213 | } | 209 | } | ||
| 214 | return v, nil | 210 | return v, nil | ||
| 215 | } | 211 | } | ||
A expr/gen/runner/fromjson_errors.mjs +36 -0
| @@ -0,0 +1,36 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | // | |
| 3 | + | // Writes testdata/fromjson_errors.json: for the inputs of testdata/fromjson_cases.json | |
| 4 | + | // that the runner's fromJSON rejects, plus a few hand-picked ones (blank input, a | |
| 5 | + | // bad character on a later line or deep in a document), the message of the | |
| 6 | + | // JsonReaderException. TestFromJSONErrorsMatchRunner replays it. | |
| 7 | + | // | |
| 8 | + | // node expr/gen/runner/fromjson_errors.mjs > expr/testdata/fromjson_errors.json | |
| 9 | + | // | |
| 10 | + | // Needs the built driver, see README.md. | |
| 11 | + | ||
| 12 | + | import { readFileSync, writeFileSync, mkdtempSync, rmSync } from "node:fs"; | |
| 13 | + | import { tmpdir } from "node:os"; | |
| 14 | + | import { join } from "node:path"; | |
| 15 | + | import { runDriver, here, checkEnvironment } from "./env.mjs"; | |
| 16 | + | ||
| 17 | + | checkEnvironment(); | |
| 18 | + | const cases = JSON.parse(readFileSync(join(here, "../../testdata/fromjson_cases.json"), "utf8")); | |
| 19 | + | const extra = [ | |
| 20 | + | "", " ", "\n", "not json", " not json", "\nnot json", "[x]", "[1,x]", '{"a":x}', "[\n x]", '{"a b":x}', '{"a.b":[1,x]}', | |
| 21 | + | "{\"it's\":x}", '{"a":{"b":[1,2,x]}}', "[[x]]", "{", "[", "[1,2", '{"a":1', '"abc', "{'a'}", "[1 2]", '{"a":1 "b":2}', | |
| 22 | + | "tru", '{"k":tru}', '"\\q"', "[1,\n2,\n x]", " \n x", '{"a":\n\n [1,\n x]}', | |
| 23 | + | ]; | |
| 24 | + | const inputs = [...new Set([...cases.filter(c => !c.ok).map(c => c.in), ...extra])]; | |
| 25 | + | const lit = s => "'" + s.replaceAll("'", "''") + "'"; | |
| 26 | + | const tmp = mkdtempSync(join(tmpdir(), "fje-")); | |
| 27 | + | writeFileSync(join(tmp, "in.json"), JSON.stringify({ contexts: {}, cases: inputs.map(s => ({ e: `fromJSON(${lit(s)})` })) })); | |
| 28 | + | const out = runDriver([join(tmp, "in.json")]).split("\n").filter(Boolean); | |
| 29 | + | rmSync(tmp, { recursive: true }); | |
| 30 | + | if (out.length !== inputs.length) throw new Error("answer count"); | |
| 31 | + | const res = []; | |
| 32 | + | inputs.forEach((s, i) => { | |
| 33 | + | const r = JSON.parse(out[i]); | |
| 34 | + | if (r.err && r.err.t === "eval") res.push({ in: s, msg: r.err.m }); | |
| 35 | + | }); | |
| 36 | + | process.stdout.write(JSON.stringify(res)); | |
| @@ -0,0 +1,36 @@ | |||||
| 1 | + | // SPDX-License-Identifier: MIT | |||
| 2 | + | // | |||
| 3 | + | // Writes testdata/fromjson_errors.json: for the inputs of testdata/fromjson_cases.json | |||
| 4 | + | // that the runner's fromJSON rejects, plus a few hand-picked ones (blank input, a | |||
| 5 | + | // bad character on a later line or deep in a document), the message of the | |||
| 6 | + | // JsonReaderException. TestFromJSONErrorsMatchRunner replays it. | |||
| 7 | + | // | |||
| 8 | + | // node expr/gen/runner/fromjson_errors.mjs > expr/testdata/fromjson_errors.json | |||
| 9 | + | // | |||
| 10 | + | // Needs the built driver, see README.md. | |||
| 11 | + | ||||
| 12 | + | import { readFileSync, writeFileSync, mkdtempSync, rmSync } from "node:fs"; | |||
| 13 | + | import { tmpdir } from "node:os"; | |||
| 14 | + | import { join } from "node:path"; | |||
| 15 | + | import { runDriver, here, checkEnvironment } from "./env.mjs"; | |||
| 16 | + | ||||
| 17 | + | checkEnvironment(); | |||
| 18 | + | const cases = JSON.parse(readFileSync(join(here, "../../testdata/fromjson_cases.json"), "utf8")); | |||
| 19 | + | const extra = [ | |||
| 20 | + | "", " ", "\n", "not json", " not json", "\nnot json", "[x]", "[1,x]", '{"a":x}', "[\n x]", '{"a b":x}', '{"a.b":[1,x]}', | |||
| 21 | + | "{\"it's\":x}", '{"a":{"b":[1,2,x]}}', "[[x]]", "{", "[", "[1,2", '{"a":1', '"abc', "{'a'}", "[1 2]", '{"a":1 "b":2}', | |||
| 22 | + | "tru", '{"k":tru}', '"\\q"', "[1,\n2,\n x]", " \n x", '{"a":\n\n [1,\n x]}', | |||
| 23 | + | ]; | |||
| 24 | + | const inputs = [...new Set([...cases.filter(c => !c.ok).map(c => c.in), ...extra])]; | |||
| 25 | + | const lit = s => "'" + s.replaceAll("'", "''") + "'"; | |||
| 26 | + | const tmp = mkdtempSync(join(tmpdir(), "fje-")); | |||
| 27 | + | writeFileSync(join(tmp, "in.json"), JSON.stringify({ contexts: {}, cases: inputs.map(s => ({ e: `fromJSON(${lit(s)})` })) })); | |||
| 28 | + | const out = runDriver([join(tmp, "in.json")]).split("\n").filter(Boolean); | |||
| 29 | + | rmSync(tmp, { recursive: true }); | |||
| 30 | + | if (out.length !== inputs.length) throw new Error("answer count"); | |||
| 31 | + | const res = []; | |||
| 32 | + | inputs.forEach((s, i) => { | |||
| 33 | + | const r = JSON.parse(out[i]); | |||
| 34 | + | if (r.err && r.err.t === "eval") res.push({ in: s, msg: r.err.m }); | |||
| 35 | + | }); | |||
| 36 | + | process.stdout.write(JSON.stringify(res)); | |||
M expr/json.go +314 -41
| @@ -47,6 +47,9 @@ func FromJSON(data []byte) (Value, error) { | |||
| 47 | 47 | } | |
| 48 | 48 | p := &jsonParser{s: s} | |
| 49 | 49 | p.skipSpace() | |
| 50 | + | if p.i >= len(p.s) { | |
| 51 | + | return nil, p.fail("Error reading JToken from JsonReader.") | |
| 52 | + | } | |
| 50 | 53 | return p.value(0) | |
| 51 | 54 | } | |
| 52 | 55 | ||
| @@ -56,14 +59,163 @@ const jsonReaderMaxDepth = 64 | |||
| 56 | 59 | type jsonParser struct { | |
| 57 | 60 | s string | |
| 58 | 61 | i int | |
| 62 | + | // started is false until the first token is read (Json.NET's Start state): | |
| 63 | + | // an error at the first column of the text then reports line 0. | |
| 64 | + | started bool | |
| 65 | + | // stack is the open containers, for the path of an error. | |
| 66 | + | stack []jsonFrame | |
| 67 | + | } | |
| 68 | + | ||
| 69 | + | // jsonFrame is an open array or object, as far as the path of an error needs. | |
| 70 | + | type jsonFrame struct { | |
| 71 | + | obj bool | |
| 72 | + | n int // array: values read so far | |
| 73 | + | name string // object: the current property name | |
| 74 | + | has bool | |
| 75 | + | } | |
| 76 | + | ||
| 77 | + | // JSONError is a failure of FromJSON. Its text is the message of the | |
| 78 | + | // Newtonsoft.Json.JsonReaderException the runner raises for the same input, | |
| 79 | + | // "<reason> Path '<path>', line <n>, position <n>.", which GitHub prints. The | |
| 80 | + | // reasons, paths and positions are reproduced for the failures seen in | |
| 81 | + | // practice (an unexpected character, a truncated text, a bad literal or number, | |
| 82 | + | // an unterminated string...); for the rarer ones the reason or the position may | |
| 83 | + | // differ from Json.NET's, but the text keeps the same form. See PORTING.md. | |
| 84 | + | type JSONError struct { | |
| 85 | + | // Reason is the message without the position, e.g. "Unexpected character | |
| 86 | + | // encountered while parsing value: n." | |
| 87 | + | Reason string | |
| 88 | + | // Path is the JSON path of the last token read ("a.b[0]"), "" at the root. | |
| 89 | + | Path string | |
| 90 | + | // Line and Position are Json.NET's JsonTextReader.LineNumber and | |
| 91 | + | // LinePosition: 1-based lines (0 before the first token), 0-based | |
| 92 | + | // positions, counted in UTF-16 units. | |
| 93 | + | Line, Position int | |
| 94 | + | } | |
| 95 | + | ||
| 96 | + | func (e *JSONError) Error() string { | |
| 97 | + | return fmt.Sprintf("%s Path '%s', line %d, position %d.", e.Reason, e.Path, e.Line, e.Position) | |
| 98 | + | } | |
| 99 | + | ||
| 100 | + | // ExceptionClass is the .NET class of the exception, which GitHub prints in | |
| 101 | + | // front of the message when it does not know where the expression is. | |
| 102 | + | func (e *JSONError) ExceptionClass() string { return "Newtonsoft.Json.JsonReaderException" } | |
| 103 | + | ||
| 104 | + | // fail builds the error for the current position. | |
| 105 | + | func (p *jsonParser) fail(reason string) error { | |
| 106 | + | line, pos := 1, 0 | |
| 107 | + | units := 0 | |
| 108 | + | for i := 0; i < p.i && i < len(p.s); { | |
| 109 | + | r, w := utf8.DecodeRuneInString(p.s[i:]) | |
| 110 | + | i += w | |
| 111 | + | switch { | |
| 112 | + | case r == '\n', r == '\r' && (i >= len(p.s) || p.s[i] != '\n'): | |
| 113 | + | line++ | |
| 114 | + | units = 0 | |
| 115 | + | case r == '\r': | |
| 116 | + | // the line break is counted with the following \n | |
| 117 | + | units++ | |
| 118 | + | default: | |
| 119 | + | units++ | |
| 120 | + | if r >= 0x10000 { | |
| 121 | + | units++ | |
| 122 | + | } | |
| 123 | + | } | |
| 124 | + | } | |
| 125 | + | pos = units | |
| 126 | + | if !p.started && pos == 0 { | |
| 127 | + | line = 0 | |
| 128 | + | } | |
| 129 | + | return &JSONError{Reason: reason, Path: p.path(), Line: line, Position: pos} | |
| 130 | + | } | |
| 131 | + | ||
| 132 | + | // path is JsonReader.Path: the property names and indexes of the open | |
| 133 | + | // containers, the container being read only once it has a current element. | |
| 134 | + | func (p *jsonParser) path() string { | |
| 135 | + | var sb strings.Builder | |
| 136 | + | for _, f := range p.stack { | |
| 137 | + | switch { | |
| 138 | + | case f.obj && f.has: | |
| 139 | + | if strings.ContainsAny(f.name, ". '/\"[]()\t\n\r\f\b\\\u0085\u2028\u2029") { | |
| 140 | + | sb.WriteString("['" + strings.NewReplacer("\\", "\\\\", "'", "\\'").Replace(f.name) + "']") | |
| 141 | + | } else { | |
| 142 | + | if sb.Len() > 0 { | |
| 143 | + | sb.WriteByte('.') | |
| 144 | + | } | |
| 145 | + | sb.WriteString(f.name) | |
| 146 | + | } | |
| 147 | + | case !f.obj && f.n > 0: | |
| 148 | + | fmt.Fprintf(&sb, "[%d]", f.n-1) | |
| 149 | + | } | |
| 150 | + | } | |
| 151 | + | return sb.String() | |
| 152 | + | } | |
| 153 | + | ||
| 154 | + | func (p *jsonParser) top() *jsonFrame { | |
| 155 | + | if len(p.stack) == 0 { | |
| 156 | + | return nil | |
| 157 | + | } | |
| 158 | + | return &p.stack[len(p.stack)-1] | |
| 159 | + | } | |
| 160 | + | ||
| 161 | + | // valueRead counts a finished scalar in the open array. | |
| 162 | + | func (p *jsonParser) valueRead() { | |
| 163 | + | p.started = true | |
| 164 | + | if f := p.top(); f != nil && !f.obj { | |
| 165 | + | f.n++ | |
| 166 | + | } | |
| 59 | 167 | } | |
| 60 | 168 | ||
| 169 | + | // unexpected is the error for a character that cannot start a value (or, at | |
| 170 | + | // the end of the text, for a text that stops too early). | |
| 61 | 171 | func (p *jsonParser) unexpected() error { | |
| 62 | 172 | if p.i >= len(p.s) { | |
| 63 | - | return errors.New("unexpected end of JSON input") | |
| 173 | + | return p.fail("Unexpected end when reading JSON.") | |
| 174 | + | } | |
| 175 | + | r, _ := utf8.DecodeRuneInString(p.s[p.i:]) | |
| 176 | + | return p.fail("Unexpected character encountered while parsing value: " + string(r) + ".") | |
| 177 | + | } | |
| 178 | + | ||
| 179 | + | // unexpectedAfter is the error for a character where a separator or the end of | |
| 180 | + | // the container should be. | |
| 181 | + | func (p *jsonParser) unexpectedAfter() error { | |
| 182 | + | if p.i >= len(p.s) { | |
| 183 | + | return p.unterminated() | |
| 64 | 184 | } | |
| 65 | 185 | r, _ := utf8.DecodeRuneInString(p.s[p.i:]) | |
| 66 | - | return fmt.Errorf("unexpected character %q in JSON at position %d", r, p.i) | |
| 186 | + | return p.fail("After parsing a value an unexpected character was encountered: " + string(r) + ".") | |
| 187 | + | } | |
| 188 | + | ||
| 189 | + | // unterminated is the error for a text that ends inside an array or object. | |
| 190 | + | func (p *jsonParser) unterminated() error { | |
| 191 | + | f := p.top() | |
| 192 | + | if f == nil { | |
| 193 | + | return p.fail("Unexpected end when reading JSON.") | |
| 194 | + | } | |
| 195 | + | kind := "JArray" | |
| 196 | + | if f.obj { | |
| 197 | + | kind = "JObject" | |
| 198 | + | } | |
| 199 | + | if (f.obj && !f.has) || (!f.obj && f.n == 0) { | |
| 200 | + | return p.fail("Error reading " + kind + " from JsonReader.") | |
| 201 | + | } | |
| 202 | + | return p.fail("Unexpected end of content while loading " + kind + ".") | |
| 203 | + | } | |
| 204 | + | ||
| 205 | + | // badLiteral is the error for a word that starts like true, false, null, NaN, | |
| 206 | + | // Infinity or undefined but is not one: Json.NET compares the characters one | |
| 207 | + | // by one and fails at the first that differs. | |
| 208 | + | func (p *jsonParser) badLiteral(word, kind string) error { | |
| 209 | + | if len(p.s)-p.i < len(word) { | |
| 210 | + | p.i = len(p.s) | |
| 211 | + | return p.fail("Unexpected end when reading JSON.") | |
| 212 | + | } | |
| 213 | + | k := 0 | |
| 214 | + | for k < len(word) && p.s[p.i+k] == word[k] { | |
| 215 | + | k++ | |
| 216 | + | } | |
| 217 | + | p.i += k | |
| 218 | + | return p.fail("Error parsing " + kind + " value.") | |
| 67 | 219 | } | |
| 68 | 220 | ||
| 69 | 221 | func (p *jsonParser) skipSpace() { | |
| @@ -112,9 +264,28 @@ func (p *jsonParser) value(depth int) (Value, error) { | |||
| 112 | 264 | if err != nil { | |
| 113 | 265 | return nil, err | |
| 114 | 266 | } | |
| 267 | + | p.valueRead() | |
| 115 | 268 | return String(s), nil | |
| 116 | 269 | case c == '-' || c == '.' || c >= '0' && c <= '9': | |
| 117 | - | return p.number() | |
| 270 | + | v, err := p.number() | |
| 271 | + | if err == nil { | |
| 272 | + | p.valueRead() | |
| 273 | + | } | |
| 274 | + | return v, err | |
| 275 | + | case c == '/': | |
| 276 | + | p.i++ | |
| 277 | + | if p.i >= len(p.s) { | |
| 278 | + | return nil, p.fail("Unexpected end while parsing comment.") | |
| 279 | + | } | |
| 280 | + | if p.s[p.i] != '*' { | |
| 281 | + | r, _ := utf8.DecodeRuneInString(p.s[p.i:]) | |
| 282 | + | return nil, p.fail("Error parsing comment. Expected: *, got " + string(r) + ".") | |
| 283 | + | } | |
| 284 | + | p.i-- | |
| 285 | + | return nil, p.fail("Comments are not supported by this port.") | |
| 286 | + | case c == ']' && len(p.stack) == 0: | |
| 287 | + | p.i++ | |
| 288 | + | return nil, p.fail("JsonToken EndArray is not valid for closing JsonType None.") | |
| 118 | 289 | } | |
| 119 | 290 | if c := p.s[p.i]; c == ',' && depth == 0 { | |
| 120 | 291 | // A comma where the document should start is an undefined value, | |
| @@ -124,41 +295,68 @@ func (p *jsonParser) value(depth int) (Value, error) { | |||
| 124 | 295 | } | |
| 125 | 296 | start := p.i | |
| 126 | 297 | var v Value | |
| 298 | + | kind := "" | |
| 127 | 299 | switch word := p.token(); word { | |
| 128 | 300 | case "true": | |
| 129 | - | v = Boolean(true) | |
| 301 | + | v, kind = Boolean(true), "boolean" | |
| 130 | 302 | case "false": | |
| 131 | - | v = Boolean(false) | |
| 303 | + | v, kind = Boolean(false), "boolean" | |
| 132 | 304 | case "null": | |
| 133 | - | v = Null{} | |
| 305 | + | v, kind = Null{}, "null" | |
| 134 | 306 | case "undefined": | |
| 135 | - | v = String("") | |
| 307 | + | v, kind = String(""), "undefined" | |
| 136 | 308 | case "NaN": | |
| 137 | - | v = Number(nan) | |
| 309 | + | v, kind = Number(nan), "NaN" | |
| 138 | 310 | case "Infinity": | |
| 139 | - | v = Number(posInf) | |
| 311 | + | v, kind = Number(posInf), "Infinity" | |
| 140 | 312 | default: | |
| 141 | 313 | p.i = start | |
| 142 | - | return nil, p.unexpected() | |
| 314 | + | return nil, p.literalError() | |
| 143 | 315 | } | |
| 144 | - | if err := p.afterLiteral(); err != nil { | |
| 316 | + | if err := p.afterLiteral(kind); err != nil { | |
| 145 | 317 | return nil, err | |
| 146 | 318 | } | |
| 319 | + | p.valueRead() | |
| 147 | 320 | return v, nil | |
| 148 | 321 | } | |
| 149 | 322 | ||
| 323 | + | // literalError explains a word that is not a literal, by its first letter. | |
| 324 | + | func (p *jsonParser) literalError() error { | |
| 325 | + | next := byte(0) | |
| 326 | + | if p.i+1 < len(p.s) { | |
| 327 | + | next = p.s[p.i+1] | |
| 328 | + | } | |
| 329 | + | switch p.s[p.i] { | |
| 330 | + | case 't': | |
| 331 | + | return p.badLiteral("true", "boolean") | |
| 332 | + | case 'f': | |
| 333 | + | return p.badLiteral("false", "boolean") | |
| 334 | + | case 'n': | |
| 335 | + | if next == 'u' { | |
| 336 | + | return p.badLiteral("null", "null") | |
| 337 | + | } | |
| 338 | + | case 'N': | |
| 339 | + | return p.badLiteral("NaN", "NaN") | |
| 340 | + | case 'I': | |
| 341 | + | return p.badLiteral("Infinity", "Infinity") | |
| 342 | + | case 'u': | |
| 343 | + | return p.badLiteral("undefined", "undefined") | |
| 344 | + | } | |
| 345 | + | return p.unexpected() | |
| 346 | + | } | |
| 347 | + | ||
| 150 | 348 | // afterLiteral checks the character that follows a word literal (true, null, | |
| 151 | 349 | // NaN...): Json.NET only accepts a "/" there when it starts a comment, and | |
| 152 | 350 | // comments are not ported, so any "/" is an error here. Anything else is left | |
| 153 | 351 | // for the caller (white space, a comma, a closing bracket, or trailing text). | |
| 154 | - | func (p *jsonParser) afterLiteral() error { | |
| 352 | + | func (p *jsonParser) afterLiteral(kind string) error { | |
| 155 | 353 | if p.i >= len(p.s) || p.s[p.i] != '/' { | |
| 156 | 354 | return nil | |
| 157 | 355 | } | |
| 158 | 356 | if p.i+1 < len(p.s) && (p.s[p.i+1] == '/' || p.s[p.i+1] == '*') { | |
| 159 | - | return fmt.Errorf("comments are not supported (position %d in JSON)", p.i) | |
| 357 | + | return p.fail("Comments are not supported by this port.") | |
| 160 | 358 | } | |
| 161 | - | return p.unexpected() | |
| 359 | + | return p.fail("Error parsing " + kind + " value.") | |
| 162 | 360 | } | |
| 163 | 361 | ||
| 164 | 362 | // number reads a number token: decimal with optional sign, fraction and | |
| @@ -166,9 +364,17 @@ func (p *jsonParser) afterLiteral() error { | |||
| 166 | 364 | // 0x hexadecimal integer. | |
| 167 | 365 | func (p *jsonParser) number() (Value, error) { | |
| 168 | 366 | start := p.i | |
| 169 | - | tok := p.token() | |
| 367 | + | if strings.HasPrefix(p.s[p.i:], "-I") { | |
| 368 | + | if !strings.HasPrefix(p.s[p.i:], "-Infinity") { | |
| 369 | + | return nil, p.badLiteral("-Infinity", "-Infinity") | |
| 370 | + | } | |
| 371 | + | } | |
| 372 | + | tok, err := p.numberToken() | |
| 373 | + | if err != nil { | |
| 374 | + | return nil, err | |
| 375 | + | } | |
| 170 | 376 | if tok == "-Infinity" { | |
| 171 | - | if err := p.afterLiteral(); err != nil { | |
| 377 | + | if err := p.afterLiteral("-Infinity"); err != nil { | |
| 172 | 378 | return nil, err | |
| 173 | 379 | } | |
| 174 | 380 | return Number(math.Inf(-1)), nil | |
| @@ -179,8 +385,7 @@ func (p *jsonParser) number() (Value, error) { | |||
| 179 | 385 | return Number(float64(int64(n))), nil | |
| 180 | 386 | } | |
| 181 | 387 | } | |
| 182 | - | p.i = start | |
| 183 | - | return nil, fmt.Errorf("input string %q is not a valid number", tok) | |
| 388 | + | return nil, p.badNumber(start, tok) | |
| 184 | 389 | } | |
| 185 | 390 | // A non-negative number with a leading zero is octal when it has only | |
| 186 | 391 | // digits (010 is 8, 08 is an error) and an error unless the zero is | |
| @@ -189,15 +394,13 @@ func (p *jsonParser) number() (Value, error) { | |||
| 189 | 394 | if len(tok) > 1 && tok[0] == '0' && tok[1] != '.' && tok[1] != 'e' && tok[1] != 'E' { | |
| 190 | 395 | v, ok := parseOctal64(tok[1:]) | |
| 191 | 396 | if !ok { | |
| 192 | - | p.i = start | |
| 193 | - | return nil, fmt.Errorf("input string %q is not a valid number", tok) | |
| 397 | + | return nil, p.badNumber(start, tok) | |
| 194 | 398 | } | |
| 195 | 399 | return Number(v), nil | |
| 196 | 400 | } | |
| 197 | 401 | body := strings.TrimPrefix(tok, "-") | |
| 198 | 402 | if !isDecimalLiteral(body) { | |
| 199 | - | p.i = start | |
| 200 | - | return nil, fmt.Errorf("input string %q is not a valid number", tok) | |
| 403 | + | return nil, p.badNumber(start, tok) | |
| 201 | 404 | } | |
| 202 | 405 | if isBigInteger(body) { | |
| 203 | 406 | // Json.NET reads an integer that does not fit in 64 bits as a | |
| @@ -216,6 +419,48 @@ func (p *jsonParser) number() (Value, error) { | |||
| 216 | 419 | return Number(f), nil | |
| 217 | 420 | } | |
| 218 | 421 | ||
| 422 | + | // numberToken reads the characters of a number as JsonTextReader does: those | |
| 423 | + | // of a fixed set (digits, signs, ".", a-f, A-F, x, X) up to a separator; any | |
| 424 | + | // other character is an error at its own position. | |
| 425 | + | func (p *jsonParser) numberToken() (string, error) { | |
| 426 | + | start := p.i | |
| 427 | + | for p.i < len(p.s) { | |
| 428 | + | c := p.s[p.i] | |
| 429 | + | switch { | |
| 430 | + | case c >= '0' && c <= '9', c >= 'a' && c <= 'f', c >= 'A' && c <= 'F', | |
| 431 | + | c == '+', c == '-', c == '.', c == 'x', c == 'X', c == 'I', c == 'n', c == 'i', c == 't', c == 'y': | |
| 432 | + | if c == 'I' || c == 'n' || c == 'i' || c == 't' || c == 'y' { | |
| 433 | + | // only the letters of -Infinity are read on; anything else | |
| 434 | + | // is not in the set | |
| 435 | + | if !strings.HasPrefix(p.s[start:], "-Infinity") { | |
| 436 | + | return "", p.numberChar() | |
| 437 | + | } | |
| 438 | + | } | |
| 439 | + | p.i++ | |
| 440 | + | default: | |
| 441 | + | r, _ := utf8.DecodeRuneInString(p.s[p.i:]) | |
| 442 | + | if isJSONSeparator(r) { | |
| 443 | + | return p.s[start:p.i], nil | |
| 444 | + | } | |
| 445 | + | return "", p.numberChar() | |
| 446 | + | } | |
| 447 | + | } | |
| 448 | + | return p.s[start:p.i], nil | |
| 449 | + | } | |
| 450 | + | ||
| 451 | + | func (p *jsonParser) numberChar() error { | |
| 452 | + | r, _ := utf8.DecodeRuneInString(p.s[p.i:]) | |
| 453 | + | return p.fail("Unexpected character encountered while parsing number: " + string(r) + ".") | |
| 454 | + | } | |
| 455 | + | ||
| 456 | + | // badNumber is the error for a malformed number token; Json.NET reports it at | |
| 457 | + | // the end of the token, with the element already counted in the path. | |
| 458 | + | func (p *jsonParser) badNumber(start int, tok string) error { | |
| 459 | + | p.i = start + len(tok) | |
| 460 | + | p.valueRead() | |
| 461 | + | return p.fail("Input string '" + tok + "' is not a valid number.") | |
| 462 | + | } | |
| 463 | + | ||
| 219 | 464 | // parseOctal64 reads octal digits as Convert.ToInt64(s, 8) does: leading | |
| 220 | 465 | // zeros are free, at most 64 significant bits, and a value of 2^63 or more | |
| 221 | 466 | // wraps to a negative number (1777777777777777777777 is -1). | |
| @@ -254,7 +499,7 @@ func (p *jsonParser) str() (string, error) { | |||
| 254 | 499 | var b strings.Builder | |
| 255 | 500 | for { | |
| 256 | 501 | if p.i >= len(p.s) { | |
| 257 | - | return "", errors.New("unterminated string in JSON") | |
| 502 | + | return "", p.fail("Unterminated string. Expected delimiter: " + string(quote) + ".") | |
| 258 | 503 | } | |
| 259 | 504 | c := p.s[p.i] | |
| 260 | 505 | if c == quote { | |
| @@ -268,7 +513,7 @@ func (p *jsonParser) str() (string, error) { | |||
| 268 | 513 | } | |
| 269 | 514 | p.i++ | |
| 270 | 515 | if p.i >= len(p.s) { | |
| 271 | - | return "", errors.New("unterminated string in JSON") | |
| 516 | + | return "", p.fail("Unterminated string. Expected delimiter: " + string(quote) + ".") | |
| 272 | 517 | } | |
| 273 | 518 | e := p.s[p.i] | |
| 274 | 519 | p.i++ | |
| @@ -308,17 +553,17 @@ func (p *jsonParser) str() (string, error) { | |||
| 308 | 553 | } | |
| 309 | 554 | b.WriteRune(r) | |
| 310 | 555 | default: | |
| 311 | - | p.i -= 2 | |
| 312 | - | return "", fmt.Errorf("bad JSON escape sequence \\%c at position %d", e, p.i) | |
| 556 | + | return "", p.fail("Bad JSON escape sequence: \\" + string(e) + ".") | |
| 313 | 557 | } | |
| 314 | 558 | } | |
| 315 | 559 | } | |
| 316 | 560 | ||
| 317 | 561 | func (p *jsonParser) hex4() (rune, error) { | |
| 318 | - | if p.i+4 > len(p.s) { | |
| 319 | - | p.i = len(p.s) | |
| 320 | - | return 0, p.unexpected() | |
| 562 | + | // JsonTextReader needs a character after the four digits (EnsureChars). | |
| 563 | + | if p.i+4 >= len(p.s) { | |
| 564 | + | return 0, p.fail("Unexpected end while parsing Unicode escape sequence.") | |
| 321 | 565 | } | |
| 566 | + | first := p.i | |
| 322 | 567 | var r rune | |
| 323 | 568 | for k := 0; k < 4; k++ { | |
| 324 | 569 | c := p.s[p.i] | |
| @@ -331,7 +576,8 @@ func (p *jsonParser) hex4() (rune, error) { | |||
| 331 | 576 | case c >= 'A' && c <= 'F': | |
| 332 | 577 | d = c - 'A' + 10 | |
| 333 | 578 | default: | |
| 334 | - | return 0, p.unexpected() | |
| 579 | + | p.i = first | |
| 580 | + | return 0, p.fail("Invalid Unicode escape sequence: \\u" + p.s[first:first+4] + ".") | |
| 335 | 581 | } | |
| 336 | 582 | r = r<<4 | rune(d) | |
| 337 | 583 | p.i++ | |
| @@ -341,22 +587,26 @@ func (p *jsonParser) hex4() (rune, error) { | |||
| 341 | 587 | ||
| 342 | 588 | func (p *jsonParser) array(depth int) (Value, error) { | |
| 343 | 589 | if depth > jsonReaderMaxDepth { | |
| 344 | - | return nil, fmt.Errorf("the reader's MaxDepth of %d has been exceeded", jsonReaderMaxDepth) | |
| 590 | + | return nil, p.fail(fmt.Sprintf("The reader's MaxDepth of %d has been exceeded.", jsonReaderMaxDepth)) | |
| 345 | 591 | } | |
| 346 | 592 | p.i++ | |
| 593 | + | p.valueRead() | |
| 594 | + | p.stack = append(p.stack, jsonFrame{}) | |
| 347 | 595 | arr := &Array{} | |
| 348 | 596 | for { | |
| 349 | 597 | p.skipSpace() | |
| 350 | 598 | if p.i >= len(p.s) { | |
| 351 | - | return nil, p.unexpected() | |
| 599 | + | return nil, p.unterminated() | |
| 352 | 600 | } | |
| 353 | 601 | switch p.s[p.i] { | |
| 354 | 602 | case ']': | |
| 355 | 603 | p.i++ | |
| 604 | + | p.pop() | |
| 356 | 605 | return arr, nil | |
| 357 | 606 | case ',': | |
| 358 | 607 | // An empty element is "undefined", which reads as an empty string. | |
| 359 | 608 | p.i++ | |
| 609 | + | p.valueRead() | |
| 360 | 610 | arr.items = append(arr.items, String("")) | |
| 361 | 611 | continue | |
| 362 | 612 | } | |
| @@ -367,34 +617,45 @@ func (p *jsonParser) array(depth int) (Value, error) { | |||
| 367 | 617 | arr.items = append(arr.items, v) | |
| 368 | 618 | p.skipSpace() | |
| 369 | 619 | if p.i >= len(p.s) { | |
| 370 | - | return nil, p.unexpected() | |
| 620 | + | return nil, p.unterminated() | |
| 371 | 621 | } | |
| 372 | 622 | switch p.s[p.i] { | |
| 373 | 623 | case ',': | |
| 374 | 624 | p.i++ | |
| 375 | 625 | case ']': | |
| 376 | 626 | default: | |
| 377 | - | return nil, p.unexpected() | |
| 627 | + | return nil, p.unexpectedAfter() | |
| 378 | 628 | } | |
| 379 | 629 | } | |
| 380 | 630 | } | |
| 381 | 631 | ||
| 632 | + | // pop closes the innermost container; it counts as a value of its parent. | |
| 633 | + | func (p *jsonParser) pop() { | |
| 634 | + | p.stack = p.stack[:len(p.stack)-1] | |
| 635 | + | } | |
| 636 | + | ||
| 382 | 637 | // propertyName reads a quoted or unquoted object name. | |
| 383 | 638 | func (p *jsonParser) propertyName() (string, error) { | |
| 384 | 639 | if p.s[p.i] == '"' || p.s[p.i] == '\'' { | |
| 385 | 640 | return p.str() | |
| 386 | 641 | } | |
| 387 | 642 | start := p.i | |
| 643 | + | if r, _ := utf8.DecodeRuneInString(p.s[p.i:]); !unicode.IsLetter(r) && !unicode.IsDigit(r) && r != '_' && r != '$' { | |
| 644 | + | return "", p.fail("Invalid property identifier character: " + string(r) + ".") | |
| 645 | + | } | |
| 388 | 646 | for p.i < len(p.s) { | |
| 389 | 647 | r, w := utf8.DecodeRuneInString(p.s[p.i:]) | |
| 390 | 648 | if r == ':' || isNetSpace(r) { | |
| 391 | 649 | break | |
| 392 | 650 | } | |
| 393 | 651 | if !unicode.IsLetter(r) && !unicode.IsDigit(r) && r != '_' && r != '$' { | |
| 394 | - | return "", p.unexpected() | |
| 652 | + | return "", p.fail("Invalid JavaScript property identifier character: " + string(r) + ".") | |
| 395 | 653 | } | |
| 396 | 654 | p.i += w | |
| 397 | 655 | } | |
| 656 | + | if p.i >= len(p.s) { | |
| 657 | + | return "", p.fail("Unexpected end while parsing unquoted property name.") | |
| 658 | + | } | |
| 398 | 659 | if p.i == start { | |
| 399 | 660 | return "", p.unexpected() | |
| 400 | 661 | } | |
| @@ -403,17 +664,20 @@ func (p *jsonParser) propertyName() (string, error) { | |||
| 403 | 664 | ||
| 404 | 665 | func (p *jsonParser) object(depth int) (Value, error) { | |
| 405 | 666 | if depth > jsonReaderMaxDepth { | |
| 406 | - | return nil, fmt.Errorf("the reader's MaxDepth of %d has been exceeded", jsonReaderMaxDepth) | |
| 667 | + | return nil, p.fail(fmt.Sprintf("The reader's MaxDepth of %d has been exceeded.", jsonReaderMaxDepth)) | |
| 407 | 668 | } | |
| 408 | 669 | p.i++ | |
| 670 | + | p.valueRead() | |
| 671 | + | p.stack = append(p.stack, jsonFrame{obj: true}) | |
| 409 | 672 | d := NewDictionary() | |
| 410 | 673 | for { | |
| 411 | 674 | p.skipSpace() | |
| 412 | 675 | if p.i >= len(p.s) { | |
| 413 | - | return nil, p.unexpected() | |
| 676 | + | return nil, p.unterminated() | |
| 414 | 677 | } | |
| 415 | 678 | if p.s[p.i] == '}' { | |
| 416 | 679 | p.i++ | |
| 680 | + | p.pop() | |
| 417 | 681 | return d, nil | |
| 418 | 682 | } | |
| 419 | 683 | k, err := p.propertyName() | |
| @@ -421,11 +685,20 @@ func (p *jsonParser) object(depth int) (Value, error) { | |||
| 421 | 685 | return nil, err | |
| 422 | 686 | } | |
| 423 | 687 | p.skipSpace() | |
| 424 | - | if p.i >= len(p.s) || p.s[p.i] != ':' { | |
| 425 | - | return nil, p.unexpected() | |
| 688 | + | if p.i >= len(p.s) { | |
| 689 | + | return nil, p.unterminated() | |
| 690 | + | } | |
| 691 | + | if p.s[p.i] != ':' { | |
| 692 | + | r, _ := utf8.DecodeRuneInString(p.s[p.i:]) | |
| 693 | + | return nil, p.fail("Invalid character after parsing property name. Expected ':' but got: " + string(r) + ".") | |
| 426 | 694 | } | |
| 427 | 695 | p.i++ | |
| 696 | + | f := p.top() | |
| 697 | + | f.name, f.has = k, true | |
| 428 | 698 | p.skipSpace() | |
| 699 | + | if p.i >= len(p.s) { | |
| 700 | + | return nil, p.unterminated() | |
| 701 | + | } | |
| 429 | 702 | v, err := p.value(depth) | |
| 430 | 703 | if err != nil { | |
| 431 | 704 | return nil, err | |
| @@ -433,14 +706,14 @@ func (p *jsonParser) object(depth int) (Value, error) { | |||
| 433 | 706 | d.set(k, v) | |
| 434 | 707 | p.skipSpace() | |
| 435 | 708 | if p.i >= len(p.s) { | |
| 436 | - | return nil, p.unexpected() | |
| 709 | + | return nil, p.unterminated() | |
| 437 | 710 | } | |
| 438 | 711 | switch p.s[p.i] { | |
| 439 | 712 | case ',': | |
| 440 | 713 | p.i++ | |
| 441 | 714 | case '}': | |
| 442 | 715 | default: | |
| 443 | - | return nil, p.unexpected() | |
| 716 | + | return nil, p.unexpectedAfter() | |
| 444 | 717 | } | |
| 445 | 718 | } | |
| 446 | 719 | } | |
| @@ -47,6 +47,9 @@ func FromJSON(data []byte) (Value, error) { | |||||
| 47 | } | 47 | } | ||
| 48 | p := &jsonParser{s: s} | 48 | p := &jsonParser{s: s} | ||
| 49 | p.skipSpace() | 49 | p.skipSpace() | ||
| 50 | + | if p.i >= len(p.s) { | |||
| 51 | + | return nil, p.fail("Error reading JToken from JsonReader.") | |||
| 52 | + | } | |||
| 50 | return p.value(0) | 53 | return p.value(0) | ||
| 51 | } | 54 | } | ||
| 52 | 55 | ||||
| @@ -56,14 +59,163 @@ const jsonReaderMaxDepth = 64 | |||||
| 56 | type jsonParser struct { | 59 | type jsonParser struct { | ||
| 57 | s string | 60 | s string | ||
| 58 | i int | 61 | i int | ||
| 62 | + | // started is false until the first token is read (Json.NET's Start state): | |||
| 63 | + | // an error at the first column of the text then reports line 0. | |||
| 64 | + | started bool | |||
| 65 | + | // stack is the open containers, for the path of an error. | |||
| 66 | + | stack []jsonFrame | |||
| 67 | + | } | |||
| 68 | + | ||||
| 69 | + | // jsonFrame is an open array or object, as far as the path of an error needs. | |||
| 70 | + | type jsonFrame struct { | |||
| 71 | + | obj bool | |||
| 72 | + | n int // array: values read so far | |||
| 73 | + | name string // object: the current property name | |||
| 74 | + | has bool | |||
| 75 | + | } | |||
| 76 | + | ||||
| 77 | + | // JSONError is a failure of FromJSON. Its text is the message of the | |||
| 78 | + | // Newtonsoft.Json.JsonReaderException the runner raises for the same input, | |||
| 79 | + | // "<reason> Path '<path>', line <n>, position <n>.", which GitHub prints. The | |||
| 80 | + | // reasons, paths and positions are reproduced for the failures seen in | |||
| 81 | + | // practice (an unexpected character, a truncated text, a bad literal or number, | |||
| 82 | + | // an unterminated string...); for the rarer ones the reason or the position may | |||
| 83 | + | // differ from Json.NET's, but the text keeps the same form. See PORTING.md. | |||
| 84 | + | type JSONError struct { | |||
| 85 | + | // Reason is the message without the position, e.g. "Unexpected character | |||
| 86 | + | // encountered while parsing value: n." | |||
| 87 | + | Reason string | |||
| 88 | + | // Path is the JSON path of the last token read ("a.b[0]"), "" at the root. | |||
| 89 | + | Path string | |||
| 90 | + | // Line and Position are Json.NET's JsonTextReader.LineNumber and | |||
| 91 | + | // LinePosition: 1-based lines (0 before the first token), 0-based | |||
| 92 | + | // positions, counted in UTF-16 units. | |||
| 93 | + | Line, Position int | |||
| 94 | + | } | |||
| 95 | + | ||||
| 96 | + | func (e *JSONError) Error() string { | |||
| 97 | + | return fmt.Sprintf("%s Path '%s', line %d, position %d.", e.Reason, e.Path, e.Line, e.Position) | |||
| 98 | + | } | |||
| 99 | + | ||||
| 100 | + | // ExceptionClass is the .NET class of the exception, which GitHub prints in | |||
| 101 | + | // front of the message when it does not know where the expression is. | |||
| 102 | + | func (e *JSONError) ExceptionClass() string { return "Newtonsoft.Json.JsonReaderException" } | |||
| 103 | + | ||||
| 104 | + | // fail builds the error for the current position. | |||
| 105 | + | func (p *jsonParser) fail(reason string) error { | |||
| 106 | + | line, pos := 1, 0 | |||
| 107 | + | units := 0 | |||
| 108 | + | for i := 0; i < p.i && i < len(p.s); { | |||
| 109 | + | r, w := utf8.DecodeRuneInString(p.s[i:]) | |||
| 110 | + | i += w | |||
| 111 | + | switch { | |||
| 112 | + | case r == '\n', r == '\r' && (i >= len(p.s) || p.s[i] != '\n'): | |||
| 113 | + | line++ | |||
| 114 | + | units = 0 | |||
| 115 | + | case r == '\r': | |||
| 116 | + | // the line break is counted with the following \n | |||
| 117 | + | units++ | |||
| 118 | + | default: | |||
| 119 | + | units++ | |||
| 120 | + | if r >= 0x10000 { | |||
| 121 | + | units++ | |||
| 122 | + | } | |||
| 123 | + | } | |||
| 124 | + | } | |||
| 125 | + | pos = units | |||
| 126 | + | if !p.started && pos == 0 { | |||
| 127 | + | line = 0 | |||
| 128 | + | } | |||
| 129 | + | return &JSONError{Reason: reason, Path: p.path(), Line: line, Position: pos} | |||
| 130 | + | } | |||
| 131 | + | ||||
| 132 | + | // path is JsonReader.Path: the property names and indexes of the open | |||
| 133 | + | // containers, the container being read only once it has a current element. | |||
| 134 | + | func (p *jsonParser) path() string { | |||
| 135 | + | var sb strings.Builder | |||
| 136 | + | for _, f := range p.stack { | |||
| 137 | + | switch { | |||
| 138 | + | case f.obj && f.has: | |||
| 139 | + | if strings.ContainsAny(f.name, ". '/\"[]()\t\n\r\f\b\\\u0085\u2028\u2029") { | |||
| 140 | + | sb.WriteString("['" + strings.NewReplacer("\\", "\\\\", "'", "\\'").Replace(f.name) + "']") | |||
| 141 | + | } else { | |||
| 142 | + | if sb.Len() > 0 { | |||
| 143 | + | sb.WriteByte('.') | |||
| 144 | + | } | |||
| 145 | + | sb.WriteString(f.name) | |||
| 146 | + | } | |||
| 147 | + | case !f.obj && f.n > 0: | |||
| 148 | + | fmt.Fprintf(&sb, "[%d]", f.n-1) | |||
| 149 | + | } | |||
| 150 | + | } | |||
| 151 | + | return sb.String() | |||
| 152 | + | } | |||
| 153 | + | ||||
| 154 | + | func (p *jsonParser) top() *jsonFrame { | |||
| 155 | + | if len(p.stack) == 0 { | |||
| 156 | + | return nil | |||
| 157 | + | } | |||
| 158 | + | return &p.stack[len(p.stack)-1] | |||
| 159 | + | } | |||
| 160 | + | ||||
| 161 | + | // valueRead counts a finished scalar in the open array. | |||
| 162 | + | func (p *jsonParser) valueRead() { | |||
| 163 | + | p.started = true | |||
| 164 | + | if f := p.top(); f != nil && !f.obj { | |||
| 165 | + | f.n++ | |||
| 166 | + | } | |||
| 59 | } | 167 | } | ||
| 60 | 168 | ||||
| 169 | + | // unexpected is the error for a character that cannot start a value (or, at | |||
| 170 | + | // the end of the text, for a text that stops too early). | |||
| 61 | func (p *jsonParser) unexpected() error { | 171 | func (p *jsonParser) unexpected() error { | ||
| 62 | if p.i >= len(p.s) { | 172 | if p.i >= len(p.s) { | ||
| 63 | - | return errors.New("unexpected end of JSON input") | 173 | + | return p.fail("Unexpected end when reading JSON.") |
| 174 | + | } | |||
| 175 | + | r, _ := utf8.DecodeRuneInString(p.s[p.i:]) | |||
| 176 | + | return p.fail("Unexpected character encountered while parsing value: " + string(r) + ".") | |||
| 177 | + | } | |||
| 178 | + | ||||
| 179 | + | // unexpectedAfter is the error for a character where a separator or the end of | |||
| 180 | + | // the container should be. | |||
| 181 | + | func (p *jsonParser) unexpectedAfter() error { | |||
| 182 | + | if p.i >= len(p.s) { | |||
| 183 | + | return p.unterminated() | |||
| 64 | } | 184 | } | ||
| 65 | r, _ := utf8.DecodeRuneInString(p.s[p.i:]) | 185 | r, _ := utf8.DecodeRuneInString(p.s[p.i:]) | ||
| 66 | - | return fmt.Errorf("unexpected character %q in JSON at position %d", r, p.i) | 186 | + | return p.fail("After parsing a value an unexpected character was encountered: " + string(r) + ".") |
| 187 | + | } | |||
| 188 | + | ||||
| 189 | + | // unterminated is the error for a text that ends inside an array or object. | |||
| 190 | + | func (p *jsonParser) unterminated() error { | |||
| 191 | + | f := p.top() | |||
| 192 | + | if f == nil { | |||
| 193 | + | return p.fail("Unexpected end when reading JSON.") | |||
| 194 | + | } | |||
| 195 | + | kind := "JArray" | |||
| 196 | + | if f.obj { | |||
| 197 | + | kind = "JObject" | |||
| 198 | + | } | |||
| 199 | + | if (f.obj && !f.has) || (!f.obj && f.n == 0) { | |||
| 200 | + | return p.fail("Error reading " + kind + " from JsonReader.") | |||
| 201 | + | } | |||
| 202 | + | return p.fail("Unexpected end of content while loading " + kind + ".") | |||
| 203 | + | } | |||
| 204 | + | ||||
| 205 | + | // badLiteral is the error for a word that starts like true, false, null, NaN, | |||
| 206 | + | // Infinity or undefined but is not one: Json.NET compares the characters one | |||
| 207 | + | // by one and fails at the first that differs. | |||
| 208 | + | func (p *jsonParser) badLiteral(word, kind string) error { | |||
| 209 | + | if len(p.s)-p.i < len(word) { | |||
| 210 | + | p.i = len(p.s) | |||
| 211 | + | return p.fail("Unexpected end when reading JSON.") | |||
| 212 | + | } | |||
| 213 | + | k := 0 | |||
| 214 | + | for k < len(word) && p.s[p.i+k] == word[k] { | |||
| 215 | + | k++ | |||
| 216 | + | } | |||
| 217 | + | p.i += k | |||
| 218 | + | return p.fail("Error parsing " + kind + " value.") | |||
| 67 | } | 219 | } | ||
| 68 | 220 | ||||
| 69 | func (p *jsonParser) skipSpace() { | 221 | func (p *jsonParser) skipSpace() { | ||
| @@ -112,9 +264,28 @@ func (p *jsonParser) value(depth int) (Value, error) { | |||||
| 112 | if err != nil { | 264 | if err != nil { | ||
| 113 | return nil, err | 265 | return nil, err | ||
| 114 | } | 266 | } | ||
| 267 | + | p.valueRead() | |||
| 115 | return String(s), nil | 268 | return String(s), nil | ||
| 116 | case c == '-' || c == '.' || c >= '0' && c <= '9': | 269 | case c == '-' || c == '.' || c >= '0' && c <= '9': | ||
| 117 | - | return p.number() | 270 | + | v, err := p.number() |
| 271 | + | if err == nil { | |||
| 272 | + | p.valueRead() | |||
| 273 | + | } | |||
| 274 | + | return v, err | |||
| 275 | + | case c == '/': | |||
| 276 | + | p.i++ | |||
| 277 | + | if p.i >= len(p.s) { | |||
| 278 | + | return nil, p.fail("Unexpected end while parsing comment.") | |||
| 279 | + | } | |||
| 280 | + | if p.s[p.i] != '*' { | |||
| 281 | + | r, _ := utf8.DecodeRuneInString(p.s[p.i:]) | |||
| 282 | + | return nil, p.fail("Error parsing comment. Expected: *, got " + string(r) + ".") | |||
| 283 | + | } | |||
| 284 | + | p.i-- | |||
| 285 | + | return nil, p.fail("Comments are not supported by this port.") | |||
| 286 | + | case c == ']' && len(p.stack) == 0: | |||
| 287 | + | p.i++ | |||
| 288 | + | return nil, p.fail("JsonToken EndArray is not valid for closing JsonType None.") | |||
| 118 | } | 289 | } | ||
| 119 | if c := p.s[p.i]; c == ',' && depth == 0 { | 290 | if c := p.s[p.i]; c == ',' && depth == 0 { | ||
| 120 | // A comma where the document should start is an undefined value, | 291 | // A comma where the document should start is an undefined value, | ||
| @@ -124,41 +295,68 @@ func (p *jsonParser) value(depth int) (Value, error) { | |||||
| 124 | } | 295 | } | ||
| 125 | start := p.i | 296 | start := p.i | ||
| 126 | var v Value | 297 | var v Value | ||
| 298 | + | kind := "" | |||
| 127 | switch word := p.token(); word { | 299 | switch word := p.token(); word { | ||
| 128 | case "true": | 300 | case "true": | ||
| 129 | - | v = Boolean(true) | 301 | + | v, kind = Boolean(true), "boolean" |
| 130 | case "false": | 302 | case "false": | ||
| 131 | - | v = Boolean(false) | 303 | + | v, kind = Boolean(false), "boolean" |
| 132 | case "null": | 304 | case "null": | ||
| 133 | - | v = Null{} | 305 | + | v, kind = Null{}, "null" |
| 134 | case "undefined": | 306 | case "undefined": | ||
| 135 | - | v = String("") | 307 | + | v, kind = String(""), "undefined" |
| 136 | case "NaN": | 308 | case "NaN": | ||
| 137 | - | v = Number(nan) | 309 | + | v, kind = Number(nan), "NaN" |
| 138 | case "Infinity": | 310 | case "Infinity": | ||
| 139 | - | v = Number(posInf) | 311 | + | v, kind = Number(posInf), "Infinity" |
| 140 | default: | 312 | default: | ||
| 141 | p.i = start | 313 | p.i = start | ||
| 142 | - | return nil, p.unexpected() | 314 | + | return nil, p.literalError() |
| 143 | } | 315 | } | ||
| 144 | - | if err := p.afterLiteral(); err != nil { | 316 | + | if err := p.afterLiteral(kind); err != nil { |
| 145 | return nil, err | 317 | return nil, err | ||
| 146 | } | 318 | } | ||
| 319 | + | p.valueRead() | |||
| 147 | return v, nil | 320 | return v, nil | ||
| 148 | } | 321 | } | ||
| 149 | 322 | ||||
| 323 | + | // literalError explains a word that is not a literal, by its first letter. | |||
| 324 | + | func (p *jsonParser) literalError() error { | |||
| 325 | + | next := byte(0) | |||
| 326 | + | if p.i+1 < len(p.s) { | |||
| 327 | + | next = p.s[p.i+1] | |||
| 328 | + | } | |||
| 329 | + | switch p.s[p.i] { | |||
| 330 | + | case 't': | |||
| 331 | + | return p.badLiteral("true", "boolean") | |||
| 332 | + | case 'f': | |||
| 333 | + | return p.badLiteral("false", "boolean") | |||
| 334 | + | case 'n': | |||
| 335 | + | if next == 'u' { | |||
| 336 | + | return p.badLiteral("null", "null") | |||
| 337 | + | } | |||
| 338 | + | case 'N': | |||
| 339 | + | return p.badLiteral("NaN", "NaN") | |||
| 340 | + | case 'I': | |||
| 341 | + | return p.badLiteral("Infinity", "Infinity") | |||
| 342 | + | case 'u': | |||
| 343 | + | return p.badLiteral("undefined", "undefined") | |||
| 344 | + | } | |||
| 345 | + | return p.unexpected() | |||
| 346 | + | } | |||
| 347 | + | ||||
| 150 | // afterLiteral checks the character that follows a word literal (true, null, | 348 | // afterLiteral checks the character that follows a word literal (true, null, | ||
| 151 | // NaN...): Json.NET only accepts a "/" there when it starts a comment, and | 349 | // NaN...): Json.NET only accepts a "/" there when it starts a comment, and | ||
| 152 | // comments are not ported, so any "/" is an error here. Anything else is left | 350 | // comments are not ported, so any "/" is an error here. Anything else is left | ||
| 153 | // for the caller (white space, a comma, a closing bracket, or trailing text). | 351 | // for the caller (white space, a comma, a closing bracket, or trailing text). | ||
| 154 | - | func (p *jsonParser) afterLiteral() error { | 352 | + | func (p *jsonParser) afterLiteral(kind string) error { |
| 155 | if p.i >= len(p.s) || p.s[p.i] != '/' { | 353 | if p.i >= len(p.s) || p.s[p.i] != '/' { | ||
| 156 | return nil | 354 | return nil | ||
| 157 | } | 355 | } | ||
| 158 | if p.i+1 < len(p.s) && (p.s[p.i+1] == '/' || p.s[p.i+1] == '*') { | 356 | if p.i+1 < len(p.s) && (p.s[p.i+1] == '/' || p.s[p.i+1] == '*') { | ||
| 159 | - | return fmt.Errorf("comments are not supported (position %d in JSON)", p.i) | 357 | + | return p.fail("Comments are not supported by this port.") |
| 160 | } | 358 | } | ||
| 161 | - | return p.unexpected() | 359 | + | return p.fail("Error parsing " + kind + " value.") |
| 162 | } | 360 | } | ||
| 163 | 361 | ||||
| 164 | // number reads a number token: decimal with optional sign, fraction and | 362 | // number reads a number token: decimal with optional sign, fraction and | ||
| @@ -166,9 +364,17 @@ func (p *jsonParser) afterLiteral() error { | |||||
| 166 | // 0x hexadecimal integer. | 364 | // 0x hexadecimal integer. | ||
| 167 | func (p *jsonParser) number() (Value, error) { | 365 | func (p *jsonParser) number() (Value, error) { | ||
| 168 | start := p.i | 366 | start := p.i | ||
| 169 | - | tok := p.token() | 367 | + | if strings.HasPrefix(p.s[p.i:], "-I") { |
| 368 | + | if !strings.HasPrefix(p.s[p.i:], "-Infinity") { | |||
| 369 | + | return nil, p.badLiteral("-Infinity", "-Infinity") | |||
| 370 | + | } | |||
| 371 | + | } | |||
| 372 | + | tok, err := p.numberToken() | |||
| 373 | + | if err != nil { | |||
| 374 | + | return nil, err | |||
| 375 | + | } | |||
| 170 | if tok == "-Infinity" { | 376 | if tok == "-Infinity" { | ||
| 171 | - | if err := p.afterLiteral(); err != nil { | 377 | + | if err := p.afterLiteral("-Infinity"); err != nil { |
| 172 | return nil, err | 378 | return nil, err | ||
| 173 | } | 379 | } | ||
| 174 | return Number(math.Inf(-1)), nil | 380 | return Number(math.Inf(-1)), nil | ||
| @@ -179,8 +385,7 @@ func (p *jsonParser) number() (Value, error) { | |||||
| 179 | return Number(float64(int64(n))), nil | 385 | return Number(float64(int64(n))), nil | ||
| 180 | } | 386 | } | ||
| 181 | } | 387 | } | ||
| 182 | - | p.i = start | 388 | + | return nil, p.badNumber(start, tok) |
| 183 | - | return nil, fmt.Errorf("input string %q is not a valid number", tok) | |||
| 184 | } | 389 | } | ||
| 185 | // A non-negative number with a leading zero is octal when it has only | 390 | // A non-negative number with a leading zero is octal when it has only | ||
| 186 | // digits (010 is 8, 08 is an error) and an error unless the zero is | 391 | // digits (010 is 8, 08 is an error) and an error unless the zero is | ||
| @@ -189,15 +394,13 @@ func (p *jsonParser) number() (Value, error) { | |||||
| 189 | if len(tok) > 1 && tok[0] == '0' && tok[1] != '.' && tok[1] != 'e' && tok[1] != 'E' { | 394 | if len(tok) > 1 && tok[0] == '0' && tok[1] != '.' && tok[1] != 'e' && tok[1] != 'E' { | ||
| 190 | v, ok := parseOctal64(tok[1:]) | 395 | v, ok := parseOctal64(tok[1:]) | ||
| 191 | if !ok { | 396 | if !ok { | ||
| 192 | - | p.i = start | 397 | + | return nil, p.badNumber(start, tok) |
| 193 | - | return nil, fmt.Errorf("input string %q is not a valid number", tok) | |||
| 194 | } | 398 | } | ||
| 195 | return Number(v), nil | 399 | return Number(v), nil | ||
| 196 | } | 400 | } | ||
| 197 | body := strings.TrimPrefix(tok, "-") | 401 | body := strings.TrimPrefix(tok, "-") | ||
| 198 | if !isDecimalLiteral(body) { | 402 | if !isDecimalLiteral(body) { | ||
| 199 | - | p.i = start | 403 | + | return nil, p.badNumber(start, tok) |
| 200 | - | return nil, fmt.Errorf("input string %q is not a valid number", tok) | |||
| 201 | } | 404 | } | ||
| 202 | if isBigInteger(body) { | 405 | if isBigInteger(body) { | ||
| 203 | // Json.NET reads an integer that does not fit in 64 bits as a | 406 | // Json.NET reads an integer that does not fit in 64 bits as a | ||
| @@ -216,6 +419,48 @@ func (p *jsonParser) number() (Value, error) { | |||||
| 216 | return Number(f), nil | 419 | return Number(f), nil | ||
| 217 | } | 420 | } | ||
| 218 | 421 | ||||
| 422 | + | // numberToken reads the characters of a number as JsonTextReader does: those | |||
| 423 | + | // of a fixed set (digits, signs, ".", a-f, A-F, x, X) up to a separator; any | |||
| 424 | + | // other character is an error at its own position. | |||
| 425 | + | func (p *jsonParser) numberToken() (string, error) { | |||
| 426 | + | start := p.i | |||
| 427 | + | for p.i < len(p.s) { | |||
| 428 | + | c := p.s[p.i] | |||
| 429 | + | switch { | |||
| 430 | + | case c >= '0' && c <= '9', c >= 'a' && c <= 'f', c >= 'A' && c <= 'F', | |||
| 431 | + | c == '+', c == '-', c == '.', c == 'x', c == 'X', c == 'I', c == 'n', c == 'i', c == 't', c == 'y': | |||
| 432 | + | if c == 'I' || c == 'n' || c == 'i' || c == 't' || c == 'y' { | |||
| 433 | + | // only the letters of -Infinity are read on; anything else | |||
| 434 | + | // is not in the set | |||
| 435 | + | if !strings.HasPrefix(p.s[start:], "-Infinity") { | |||
| 436 | + | return "", p.numberChar() | |||
| 437 | + | } | |||
| 438 | + | } | |||
| 439 | + | p.i++ | |||
| 440 | + | default: | |||
| 441 | + | r, _ := utf8.DecodeRuneInString(p.s[p.i:]) | |||
| 442 | + | if isJSONSeparator(r) { | |||
| 443 | + | return p.s[start:p.i], nil | |||
| 444 | + | } | |||
| 445 | + | return "", p.numberChar() | |||
| 446 | + | } | |||
| 447 | + | } | |||
| 448 | + | return p.s[start:p.i], nil | |||
| 449 | + | } | |||
| 450 | + | ||||
| 451 | + | func (p *jsonParser) numberChar() error { | |||
| 452 | + | r, _ := utf8.DecodeRuneInString(p.s[p.i:]) | |||
| 453 | + | return p.fail("Unexpected character encountered while parsing number: " + string(r) + ".") | |||
| 454 | + | } | |||
| 455 | + | ||||
| 456 | + | // badNumber is the error for a malformed number token; Json.NET reports it at | |||
| 457 | + | // the end of the token, with the element already counted in the path. | |||
| 458 | + | func (p *jsonParser) badNumber(start int, tok string) error { | |||
| 459 | + | p.i = start + len(tok) | |||
| 460 | + | p.valueRead() | |||
| 461 | + | return p.fail("Input string '" + tok + "' is not a valid number.") | |||
| 462 | + | } | |||
| 463 | + | ||||
| 219 | // parseOctal64 reads octal digits as Convert.ToInt64(s, 8) does: leading | 464 | // parseOctal64 reads octal digits as Convert.ToInt64(s, 8) does: leading | ||
| 220 | // zeros are free, at most 64 significant bits, and a value of 2^63 or more | 465 | // zeros are free, at most 64 significant bits, and a value of 2^63 or more | ||
| 221 | // wraps to a negative number (1777777777777777777777 is -1). | 466 | // wraps to a negative number (1777777777777777777777 is -1). | ||
| @@ -254,7 +499,7 @@ func (p *jsonParser) str() (string, error) { | |||||
| 254 | var b strings.Builder | 499 | var b strings.Builder | ||
| 255 | for { | 500 | for { | ||
| 256 | if p.i >= len(p.s) { | 501 | if p.i >= len(p.s) { | ||
| 257 | - | return "", errors.New("unterminated string in JSON") | 502 | + | return "", p.fail("Unterminated string. Expected delimiter: " + string(quote) + ".") |
| 258 | } | 503 | } | ||
| 259 | c := p.s[p.i] | 504 | c := p.s[p.i] | ||
| 260 | if c == quote { | 505 | if c == quote { | ||
| @@ -268,7 +513,7 @@ func (p *jsonParser) str() (string, error) { | |||||
| 268 | } | 513 | } | ||
| 269 | p.i++ | 514 | p.i++ | ||
| 270 | if p.i >= len(p.s) { | 515 | if p.i >= len(p.s) { | ||
| 271 | - | return "", errors.New("unterminated string in JSON") | 516 | + | return "", p.fail("Unterminated string. Expected delimiter: " + string(quote) + ".") |
| 272 | } | 517 | } | ||
| 273 | e := p.s[p.i] | 518 | e := p.s[p.i] | ||
| 274 | p.i++ | 519 | p.i++ | ||
| @@ -308,17 +553,17 @@ func (p *jsonParser) str() (string, error) { | |||||
| 308 | } | 553 | } | ||
| 309 | b.WriteRune(r) | 554 | b.WriteRune(r) | ||
| 310 | default: | 555 | default: | ||
| 311 | - | p.i -= 2 | 556 | + | return "", p.fail("Bad JSON escape sequence: \\" + string(e) + ".") |
| 312 | - | return "", fmt.Errorf("bad JSON escape sequence \\%c at position %d", e, p.i) | |||
| 313 | } | 557 | } | ||
| 314 | } | 558 | } | ||
| 315 | } | 559 | } | ||
| 316 | 560 | ||||
| 317 | func (p *jsonParser) hex4() (rune, error) { | 561 | func (p *jsonParser) hex4() (rune, error) { | ||
| 318 | - | if p.i+4 > len(p.s) { | 562 | + | // JsonTextReader needs a character after the four digits (EnsureChars). |
| 319 | - | p.i = len(p.s) | 563 | + | if p.i+4 >= len(p.s) { |
| 320 | - | return 0, p.unexpected() | 564 | + | return 0, p.fail("Unexpected end while parsing Unicode escape sequence.") |
| 321 | } | 565 | } | ||
| 566 | + | first := p.i | |||
| 322 | var r rune | 567 | var r rune | ||
| 323 | for k := 0; k < 4; k++ { | 568 | for k := 0; k < 4; k++ { | ||
| 324 | c := p.s[p.i] | 569 | c := p.s[p.i] | ||
| @@ -331,7 +576,8 @@ func (p *jsonParser) hex4() (rune, error) { | |||||
| 331 | case c >= 'A' && c <= 'F': | 576 | case c >= 'A' && c <= 'F': | ||
| 332 | d = c - 'A' + 10 | 577 | d = c - 'A' + 10 | ||
| 333 | default: | 578 | default: | ||
| 334 | - | return 0, p.unexpected() | 579 | + | p.i = first |
| 580 | + | return 0, p.fail("Invalid Unicode escape sequence: \\u" + p.s[first:first+4] + ".") | |||
| 335 | } | 581 | } | ||
| 336 | r = r<<4 | rune(d) | 582 | r = r<<4 | rune(d) | ||
| 337 | p.i++ | 583 | p.i++ | ||
| @@ -341,22 +587,26 @@ func (p *jsonParser) hex4() (rune, error) { | |||||
| 341 | 587 | ||||
| 342 | func (p *jsonParser) array(depth int) (Value, error) { | 588 | func (p *jsonParser) array(depth int) (Value, error) { | ||
| 343 | if depth > jsonReaderMaxDepth { | 589 | if depth > jsonReaderMaxDepth { | ||
| 344 | - | return nil, fmt.Errorf("the reader's MaxDepth of %d has been exceeded", jsonReaderMaxDepth) | 590 | + | return nil, p.fail(fmt.Sprintf("The reader's MaxDepth of %d has been exceeded.", jsonReaderMaxDepth)) |
| 345 | } | 591 | } | ||
| 346 | p.i++ | 592 | p.i++ | ||
| 593 | + | p.valueRead() | |||
| 594 | + | p.stack = append(p.stack, jsonFrame{}) | |||
| 347 | arr := &Array{} | 595 | arr := &Array{} | ||
| 348 | for { | 596 | for { | ||
| 349 | p.skipSpace() | 597 | p.skipSpace() | ||
| 350 | if p.i >= len(p.s) { | 598 | if p.i >= len(p.s) { | ||
| 351 | - | return nil, p.unexpected() | 599 | + | return nil, p.unterminated() |
| 352 | } | 600 | } | ||
| 353 | switch p.s[p.i] { | 601 | switch p.s[p.i] { | ||
| 354 | case ']': | 602 | case ']': | ||
| 355 | p.i++ | 603 | p.i++ | ||
| 604 | + | p.pop() | |||
| 356 | return arr, nil | 605 | return arr, nil | ||
| 357 | case ',': | 606 | case ',': | ||
| 358 | // An empty element is "undefined", which reads as an empty string. | 607 | // An empty element is "undefined", which reads as an empty string. | ||
| 359 | p.i++ | 608 | p.i++ | ||
| 609 | + | p.valueRead() | |||
| 360 | arr.items = append(arr.items, String("")) | 610 | arr.items = append(arr.items, String("")) | ||
| 361 | continue | 611 | continue | ||
| 362 | } | 612 | } | ||
| @@ -367,34 +617,45 @@ func (p *jsonParser) array(depth int) (Value, error) { | |||||
| 367 | arr.items = append(arr.items, v) | 617 | arr.items = append(arr.items, v) | ||
| 368 | p.skipSpace() | 618 | p.skipSpace() | ||
| 369 | if p.i >= len(p.s) { | 619 | if p.i >= len(p.s) { | ||
| 370 | - | return nil, p.unexpected() | 620 | + | return nil, p.unterminated() |
| 371 | } | 621 | } | ||
| 372 | switch p.s[p.i] { | 622 | switch p.s[p.i] { | ||
| 373 | case ',': | 623 | case ',': | ||
| 374 | p.i++ | 624 | p.i++ | ||
| 375 | case ']': | 625 | case ']': | ||
| 376 | default: | 626 | default: | ||
| 377 | - | return nil, p.unexpected() | 627 | + | return nil, p.unexpectedAfter() |
| 378 | } | 628 | } | ||
| 379 | } | 629 | } | ||
| 380 | } | 630 | } | ||
| 381 | 631 | ||||
| 632 | + | // pop closes the innermost container; it counts as a value of its parent. | |||
| 633 | + | func (p *jsonParser) pop() { | |||
| 634 | + | p.stack = p.stack[:len(p.stack)-1] | |||
| 635 | + | } | |||
| 636 | + | ||||
| 382 | // propertyName reads a quoted or unquoted object name. | 637 | // propertyName reads a quoted or unquoted object name. | ||
| 383 | func (p *jsonParser) propertyName() (string, error) { | 638 | func (p *jsonParser) propertyName() (string, error) { | ||
| 384 | if p.s[p.i] == '"' || p.s[p.i] == '\'' { | 639 | if p.s[p.i] == '"' || p.s[p.i] == '\'' { | ||
| 385 | return p.str() | 640 | return p.str() | ||
| 386 | } | 641 | } | ||
| 387 | start := p.i | 642 | start := p.i | ||
| 643 | + | if r, _ := utf8.DecodeRuneInString(p.s[p.i:]); !unicode.IsLetter(r) && !unicode.IsDigit(r) && r != '_' && r != '$' { | |||
| 644 | + | return "", p.fail("Invalid property identifier character: " + string(r) + ".") | |||
| 645 | + | } | |||
| 388 | for p.i < len(p.s) { | 646 | for p.i < len(p.s) { | ||
| 389 | r, w := utf8.DecodeRuneInString(p.s[p.i:]) | 647 | r, w := utf8.DecodeRuneInString(p.s[p.i:]) | ||
| 390 | if r == ':' || isNetSpace(r) { | 648 | if r == ':' || isNetSpace(r) { | ||
| 391 | break | 649 | break | ||
| 392 | } | 650 | } | ||
| 393 | if !unicode.IsLetter(r) && !unicode.IsDigit(r) && r != '_' && r != '$' { | 651 | if !unicode.IsLetter(r) && !unicode.IsDigit(r) && r != '_' && r != '$' { | ||
| 394 | - | return "", p.unexpected() | 652 | + | return "", p.fail("Invalid JavaScript property identifier character: " + string(r) + ".") |
| 395 | } | 653 | } | ||
| 396 | p.i += w | 654 | p.i += w | ||
| 397 | } | 655 | } | ||
| 656 | + | if p.i >= len(p.s) { | |||
| 657 | + | return "", p.fail("Unexpected end while parsing unquoted property name.") | |||
| 658 | + | } | |||
| 398 | if p.i == start { | 659 | if p.i == start { | ||
| 399 | return "", p.unexpected() | 660 | return "", p.unexpected() | ||
| 400 | } | 661 | } | ||
| @@ -403,17 +664,20 @@ func (p *jsonParser) propertyName() (string, error) { | |||||
| 403 | 664 | ||||
| 404 | func (p *jsonParser) object(depth int) (Value, error) { | 665 | func (p *jsonParser) object(depth int) (Value, error) { | ||
| 405 | if depth > jsonReaderMaxDepth { | 666 | if depth > jsonReaderMaxDepth { | ||
| 406 | - | return nil, fmt.Errorf("the reader's MaxDepth of %d has been exceeded", jsonReaderMaxDepth) | 667 | + | return nil, p.fail(fmt.Sprintf("The reader's MaxDepth of %d has been exceeded.", jsonReaderMaxDepth)) |
| 407 | } | 668 | } | ||
| 408 | p.i++ | 669 | p.i++ | ||
| 670 | + | p.valueRead() | |||
| 671 | + | p.stack = append(p.stack, jsonFrame{obj: true}) | |||
| 409 | d := NewDictionary() | 672 | d := NewDictionary() | ||
| 410 | for { | 673 | for { | ||
| 411 | p.skipSpace() | 674 | p.skipSpace() | ||
| 412 | if p.i >= len(p.s) { | 675 | if p.i >= len(p.s) { | ||
| 413 | - | return nil, p.unexpected() | 676 | + | return nil, p.unterminated() |
| 414 | } | 677 | } | ||
| 415 | if p.s[p.i] == '}' { | 678 | if p.s[p.i] == '}' { | ||
| 416 | p.i++ | 679 | p.i++ | ||
| 680 | + | p.pop() | |||
| 417 | return d, nil | 681 | return d, nil | ||
| 418 | } | 682 | } | ||
| 419 | k, err := p.propertyName() | 683 | k, err := p.propertyName() | ||
| @@ -421,11 +685,20 @@ func (p *jsonParser) object(depth int) (Value, error) { | |||||
| 421 | return nil, err | 685 | return nil, err | ||
| 422 | } | 686 | } | ||
| 423 | p.skipSpace() | 687 | p.skipSpace() | ||
| 424 | - | if p.i >= len(p.s) || p.s[p.i] != ':' { | 688 | + | if p.i >= len(p.s) { |
| 425 | - | return nil, p.unexpected() | 689 | + | return nil, p.unterminated() |
| 690 | + | } | |||
| 691 | + | if p.s[p.i] != ':' { | |||
| 692 | + | r, _ := utf8.DecodeRuneInString(p.s[p.i:]) | |||
| 693 | + | return nil, p.fail("Invalid character after parsing property name. Expected ':' but got: " + string(r) + ".") | |||
| 426 | } | 694 | } | ||
| 427 | p.i++ | 695 | p.i++ | ||
| 696 | + | f := p.top() | |||
| 697 | + | f.name, f.has = k, true | |||
| 428 | p.skipSpace() | 698 | p.skipSpace() | ||
| 699 | + | if p.i >= len(p.s) { | |||
| 700 | + | return nil, p.unterminated() | |||
| 701 | + | } | |||
| 429 | v, err := p.value(depth) | 702 | v, err := p.value(depth) | ||
| 430 | if err != nil { | 703 | if err != nil { | ||
| 431 | return nil, err | 704 | return nil, err | ||
| @@ -433,14 +706,14 @@ func (p *jsonParser) object(depth int) (Value, error) { | |||||
| 433 | d.set(k, v) | 706 | d.set(k, v) | ||
| 434 | p.skipSpace() | 707 | p.skipSpace() | ||
| 435 | if p.i >= len(p.s) { | 708 | if p.i >= len(p.s) { | ||
| 436 | - | return nil, p.unexpected() | 709 | + | return nil, p.unterminated() |
| 437 | } | 710 | } | ||
| 438 | switch p.s[p.i] { | 711 | switch p.s[p.i] { | ||
| 439 | case ',': | 712 | case ',': | ||
| 440 | p.i++ | 713 | p.i++ | ||
| 441 | case '}': | 714 | case '}': | ||
| 442 | default: | 715 | default: | ||
| 443 | - | return nil, p.unexpected() | 716 | + | return nil, p.unexpectedAfter() |
| 444 | } | 717 | } | ||
| 445 | } | 718 | } | ||
| 446 | } | 719 | } | ||
M expr/runner_oracle_test.go +2 -2
| @@ -116,8 +116,8 @@ func goAnswer(e string, ctxNames []string, ec EvalContext) runnerCase { | |||
| 116 | 116 | // goErrorClass names the kind of an evaluation error of this package in the | |
| 117 | 117 | // terms of runnerErr.X: "JsonReaderException" for the failures of fromJSON. | |
| 118 | 118 | func goErrorClass(err error) string { | |
| 119 | - | var ee *EvalError | |
| 120 | - | if errors.As(err, &ee) && (ee.Message == "empty input" || ee.Message == "Error parsing JSON when evaluating fromJson") { | |
| 119 | + | var je *JSONError | |
| 120 | + | if errors.As(err, &je) { | |
| 121 | 121 | return "JsonReaderException" | |
| 122 | 122 | } | |
| 123 | 123 | return "" | |
| @@ -116,8 +116,8 @@ func goAnswer(e string, ctxNames []string, ec EvalContext) runnerCase { | |||||
| 116 | // goErrorClass names the kind of an evaluation error of this package in the | 116 | // goErrorClass names the kind of an evaluation error of this package in the | ||
| 117 | // terms of runnerErr.X: "JsonReaderException" for the failures of fromJSON. | 117 | // terms of runnerErr.X: "JsonReaderException" for the failures of fromJSON. | ||
| 118 | func goErrorClass(err error) string { | 118 | func goErrorClass(err error) string { | ||
| 119 | - | var ee *EvalError | 119 | + | var je *JSONError |
| 120 | - | if errors.As(err, &ee) && (ee.Message == "empty input" || ee.Message == "Error parsing JSON when evaluating fromJson") { | 120 | + | if errors.As(err, &je) { |
| 121 | return "JsonReaderException" | 121 | return "JsonReaderException" | ||
| 122 | } | 122 | } | ||
| 123 | return "" | 123 | return "" | ||
A expr/testdata/fromjson_errors.json +1 -0 too large to show in the browser
too large to show in the browser view raw