Merge pull request 'feat(expr): fromJSON fails with Json.NET's message, as GitHub prints it' (#7) from feat/expr-newtonsoft-errors into main
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