detached 0e48653

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 VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · 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.
235235 are errors, as are `01.5`, `012e1`, `00.5` (a leading zero before `.`,
236236 `e` or `E` is only valid for a single `0`); a negative number is decimal
237237 (`-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
241248 reads `/*x*/1` as the string `x`) and `new Date(1)` constructors; both are
242249 errors here. A `/` after `null`, `true`, `false`, `NaN`, `Infinity`,
243250 `-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 decimal236 `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 becomes237 (`-010` is -10, `-01.5` is -1.5). An unpaired surrogate escape becomes
238- U+FFFD. Blank input is a `JsonReaderException` (here: "empty input", kept238+ U+FFFD. Blank input is a `JsonReaderException` ("Error reading JToken from
239- as the outer message; the Json.NET message texts are not reproduced, only239+ JsonReader."). **Error texts**: a failure is a `*JSONError` whose text is
240- that the call fails). Not ported: `/* */` and `//` comments (the runner240+ 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 are248 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 or250 `-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) {
226226 // Short-circuit: the failing call is never evaluated.
227227 {"true || fromJSON('')", "Boolean:true"},
228228 {"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."},
231231 // 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."},
234234 // Precedence: && binds tighter than ||, ! tighter than comparison.
235235 {"true || false && false", "Boolean:true"},
236236 {"(true || false) && false", "Boolean:false"},
@@ -422,11 +422,11 @@ func TestEvalBuiltinFunctions(t *testing.T) {
422422 {"fromJSON(toJSON(github.event.commits[0])).author.name", "String:Ann"},
423423
424424 // 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."},
430430 // Json.NET is lenient: trailing commas, unquoted names, leading zeros,
431431 // NaN and Infinity and anything after the first value are accepted.
432432 {"fromJSON('[1,]')", "Array:Array"},
@@ -441,7 +441,7 @@ func TestEvalBuiltinFunctions(t *testing.T) {
441441 {"fromJSON('\"\\ud83d\\ude00\"')", "String:😀"},
442442 {"fromJSON(1)", "Number:1"},
443443 {"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"
445445 {"fromJSON(1, 2)", "PARSE Too many parameters supplied: '('"},
446446
447447 // 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 // fromJSON424 // 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 // format447 // format
M expr/example_test.go +1 -1
@@ -71,7 +71,7 @@ func ExampleEvaluate() {
7171 // join(github.labels.*.name, ', ') -> Bug, docs (truthy: true)
7272 // format('{0} has {1} labels', github.ref, 2) -> refs/heads/main has 2 labels (truthy: true)
7373 // 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.
7575 }
7676
7777 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 fromJson74+ // 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) {
195195 return String(out), nil
196196 }
197197
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.
201201 func funcFromJSON(e *evaluator, c *FunctionCall) (Value, error) {
202202 a, err := e.eval(c.Args[0])
203203 if err != nil {
204204 return nil, err
205205 }
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)))
211207 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}
213209 }
214210 return v, nil
215211 }
@@ -195,21 +195,17 @@ func funcToJSON(e *evaluator, c *FunctionCall) (Value, error) {
195 return String(out), nil195 return String(out), nil
196 }196 }
197 197
198-// funcFromJSON is fromJSON(value). Blank input is "empty input"; anything198+// funcFromJSON is fromJSON(value). Anything that Json.NET cannot read, blank
199-// that is not readable by Json.NET is "Error parsing JSON when evaluating199+// 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, err204 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, nil210 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) {
4747 }
4848 p := &jsonParser{s: s}
4949 p.skipSpace()
50+ if p.i >= len(p.s) {
51+ return nil, p.fail("Error reading JToken from JsonReader.")
52+ }
5053 return p.value(0)
5154 }
5255
@@ -56,14 +59,163 @@ const jsonReaderMaxDepth = 64
5659 type jsonParser struct {
5760 s string
5861 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+ }
59167 }
60168
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).
61171 func (p *jsonParser) unexpected() error {
62172 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()
64184 }
65185 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.")
67219 }
68220
69221 func (p *jsonParser) skipSpace() {
@@ -112,9 +264,28 @@ func (p *jsonParser) value(depth int) (Value, error) {
112264 if err != nil {
113265 return nil, err
114266 }
267+ p.valueRead()
115268 return String(s), nil
116269 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.")
118289 }
119290 if c := p.s[p.i]; c == ',' && depth == 0 {
120291 // A comma where the document should start is an undefined value,
@@ -124,41 +295,68 @@ func (p *jsonParser) value(depth int) (Value, error) {
124295 }
125296 start := p.i
126297 var v Value
298+ kind := ""
127299 switch word := p.token(); word {
128300 case "true":
129- v = Boolean(true)
301+ v, kind = Boolean(true), "boolean"
130302 case "false":
131- v = Boolean(false)
303+ v, kind = Boolean(false), "boolean"
132304 case "null":
133- v = Null{}
305+ v, kind = Null{}, "null"
134306 case "undefined":
135- v = String("")
307+ v, kind = String(""), "undefined"
136308 case "NaN":
137- v = Number(nan)
309+ v, kind = Number(nan), "NaN"
138310 case "Infinity":
139- v = Number(posInf)
311+ v, kind = Number(posInf), "Infinity"
140312 default:
141313 p.i = start
142- return nil, p.unexpected()
314+ return nil, p.literalError()
143315 }
144- if err := p.afterLiteral(); err != nil {
316+ if err := p.afterLiteral(kind); err != nil {
145317 return nil, err
146318 }
319+ p.valueRead()
147320 return v, nil
148321 }
149322
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+
150348 // afterLiteral checks the character that follows a word literal (true, null,
151349 // NaN...): Json.NET only accepts a "/" there when it starts a comment, and
152350 // comments are not ported, so any "/" is an error here. Anything else is left
153351 // 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 {
155353 if p.i >= len(p.s) || p.s[p.i] != '/' {
156354 return nil
157355 }
158356 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.")
160358 }
161- return p.unexpected()
359+ return p.fail("Error parsing " + kind + " value.")
162360 }
163361
164362 // number reads a number token: decimal with optional sign, fraction and
@@ -166,9 +364,17 @@ func (p *jsonParser) afterLiteral() error {
166364 // 0x hexadecimal integer.
167365 func (p *jsonParser) number() (Value, error) {
168366 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+ }
170376 if tok == "-Infinity" {
171- if err := p.afterLiteral(); err != nil {
377+ if err := p.afterLiteral("-Infinity"); err != nil {
172378 return nil, err
173379 }
174380 return Number(math.Inf(-1)), nil
@@ -179,8 +385,7 @@ func (p *jsonParser) number() (Value, error) {
179385 return Number(float64(int64(n))), nil
180386 }
181387 }
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)
184389 }
185390 // A non-negative number with a leading zero is octal when it has only
186391 // 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) {
189394 if len(tok) > 1 && tok[0] == '0' && tok[1] != '.' && tok[1] != 'e' && tok[1] != 'E' {
190395 v, ok := parseOctal64(tok[1:])
191396 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)
194398 }
195399 return Number(v), nil
196400 }
197401 body := strings.TrimPrefix(tok, "-")
198402 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)
201404 }
202405 if isBigInteger(body) {
203406 // Json.NET reads an integer that does not fit in 64 bits as a
@@ -216,6 +419,48 @@ func (p *jsonParser) number() (Value, error) {
216419 return Number(f), nil
217420 }
218421
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+
219464 // parseOctal64 reads octal digits as Convert.ToInt64(s, 8) does: leading
220465 // zeros are free, at most 64 significant bits, and a value of 2^63 or more
221466 // wraps to a negative number (1777777777777777777777 is -1).
@@ -254,7 +499,7 @@ func (p *jsonParser) str() (string, error) {
254499 var b strings.Builder
255500 for {
256501 if p.i >= len(p.s) {
257- return "", errors.New("unterminated string in JSON")
502+ return "", p.fail("Unterminated string. Expected delimiter: " + string(quote) + ".")
258503 }
259504 c := p.s[p.i]
260505 if c == quote {
@@ -268,7 +513,7 @@ func (p *jsonParser) str() (string, error) {
268513 }
269514 p.i++
270515 if p.i >= len(p.s) {
271- return "", errors.New("unterminated string in JSON")
516+ return "", p.fail("Unterminated string. Expected delimiter: " + string(quote) + ".")
272517 }
273518 e := p.s[p.i]
274519 p.i++
@@ -308,17 +553,17 @@ func (p *jsonParser) str() (string, error) {
308553 }
309554 b.WriteRune(r)
310555 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) + ".")
313557 }
314558 }
315559 }
316560
317561 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.")
321565 }
566+ first := p.i
322567 var r rune
323568 for k := 0; k < 4; k++ {
324569 c := p.s[p.i]
@@ -331,7 +576,8 @@ func (p *jsonParser) hex4() (rune, error) {
331576 case c >= 'A' && c <= 'F':
332577 d = c - 'A' + 10
333578 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] + ".")
335581 }
336582 r = r<<4 | rune(d)
337583 p.i++
@@ -341,22 +587,26 @@ func (p *jsonParser) hex4() (rune, error) {
341587
342588 func (p *jsonParser) array(depth int) (Value, error) {
343589 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))
345591 }
346592 p.i++
593+ p.valueRead()
594+ p.stack = append(p.stack, jsonFrame{})
347595 arr := &Array{}
348596 for {
349597 p.skipSpace()
350598 if p.i >= len(p.s) {
351- return nil, p.unexpected()
599+ return nil, p.unterminated()
352600 }
353601 switch p.s[p.i] {
354602 case ']':
355603 p.i++
604+ p.pop()
356605 return arr, nil
357606 case ',':
358607 // An empty element is "undefined", which reads as an empty string.
359608 p.i++
609+ p.valueRead()
360610 arr.items = append(arr.items, String(""))
361611 continue
362612 }
@@ -367,34 +617,45 @@ func (p *jsonParser) array(depth int) (Value, error) {
367617 arr.items = append(arr.items, v)
368618 p.skipSpace()
369619 if p.i >= len(p.s) {
370- return nil, p.unexpected()
620+ return nil, p.unterminated()
371621 }
372622 switch p.s[p.i] {
373623 case ',':
374624 p.i++
375625 case ']':
376626 default:
377- return nil, p.unexpected()
627+ return nil, p.unexpectedAfter()
378628 }
379629 }
380630 }
381631
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+
382637 // propertyName reads a quoted or unquoted object name.
383638 func (p *jsonParser) propertyName() (string, error) {
384639 if p.s[p.i] == '"' || p.s[p.i] == '\'' {
385640 return p.str()
386641 }
387642 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+ }
388646 for p.i < len(p.s) {
389647 r, w := utf8.DecodeRuneInString(p.s[p.i:])
390648 if r == ':' || isNetSpace(r) {
391649 break
392650 }
393651 if !unicode.IsLetter(r) && !unicode.IsDigit(r) && r != '_' && r != '$' {
394- return "", p.unexpected()
652+ return "", p.fail("Invalid JavaScript property identifier character: " + string(r) + ".")
395653 }
396654 p.i += w
397655 }
656+ if p.i >= len(p.s) {
657+ return "", p.fail("Unexpected end while parsing unquoted property name.")
658+ }
398659 if p.i == start {
399660 return "", p.unexpected()
400661 }
@@ -403,17 +664,20 @@ func (p *jsonParser) propertyName() (string, error) {
403664
404665 func (p *jsonParser) object(depth int) (Value, error) {
405666 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))
407668 }
408669 p.i++
670+ p.valueRead()
671+ p.stack = append(p.stack, jsonFrame{obj: true})
409672 d := NewDictionary()
410673 for {
411674 p.skipSpace()
412675 if p.i >= len(p.s) {
413- return nil, p.unexpected()
676+ return nil, p.unterminated()
414677 }
415678 if p.s[p.i] == '}' {
416679 p.i++
680+ p.pop()
417681 return d, nil
418682 }
419683 k, err := p.propertyName()
@@ -421,11 +685,20 @@ func (p *jsonParser) object(depth int) (Value, error) {
421685 return nil, err
422686 }
423687 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) + ".")
426694 }
427695 p.i++
696+ f := p.top()
697+ f.name, f.has = k, true
428698 p.skipSpace()
699+ if p.i >= len(p.s) {
700+ return nil, p.unterminated()
701+ }
429702 v, err := p.value(depth)
430703 if err != nil {
431704 return nil, err
@@ -433,14 +706,14 @@ func (p *jsonParser) object(depth int) (Value, error) {
433706 d.set(k, v)
434707 p.skipSpace()
435708 if p.i >= len(p.s) {
436- return nil, p.unexpected()
709+ return nil, p.unterminated()
437710 }
438711 switch p.s[p.i] {
439712 case ',':
440713 p.i++
441714 case '}':
442715 default:
443- return nil, p.unexpected()
716+ return nil, p.unexpectedAfter()
444717 }
445718 }
446719 }
@@ -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 string60 s string
58 i int61 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, err265 return nil, err
114 }266 }
267+ p.valueRead()
115 return String(s), nil268 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.i296 start := p.i
126 var v Value297 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 = start313 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, err317 return nil, err
146 }318 }
319+ p.valueRead()
147 return v, nil320 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, and349 // 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 left350 // 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 nil354 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 and362 // 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.i366 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, err378 return nil, err
173 }379 }
174 return Number(math.Inf(-1)), nil380 return Number(math.Inf(-1)), nil
@@ -179,8 +385,7 @@ func (p *jsonParser) number() (Value, error) {
179 return Number(float64(int64(n))), nil385 return Number(float64(int64(n))), nil
180 }386 }
181 }387 }
182- p.i = start388+ 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 only390 // 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 is391 // 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 = start397+ 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), nil399 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 = start403+ 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 a406 // 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), nil419 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: leading464 // 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 more465 // 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.Builder499 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 -= 2556+ 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 rune567 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' + 10577 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, nil605 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 continue611 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.i642 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 break649 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 += w654 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, nil681 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, err685 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, err704 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 {
116116 // goErrorClass names the kind of an evaluation error of this package in the
117117 // terms of runnerErr.X: "JsonReaderException" for the failures of fromJSON.
118118 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) {
121121 return "JsonReaderException"
122122 }
123123 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 the116 // 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 *EvalError119+ 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

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 3

↑↓ moveenter openesc close