detached 28f9d6e

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

commit
28f9d6e854477ad04b274f50930c05dde81aec71
author
bnema · · 2026-10-13 04:01 UTC
committer
brice · · 2026-10-18 14:59 UTC
merge of
85fcfb7 + 0e48653 · diff against the first parent
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
refs
tag: v0.1.0
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