From 39d56745c67dd3a1458c8eab1971d5b75bcf5903 Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Tue, 14 Jan 2025 02:40:52 -0800 Subject: [PATCH] Fix minor regressions in legacy error strings The Go 1 compatability document does not guarantee that error strings remain unchanged across Go releases. However, there is a practical benefit to reducing the churn even if this is not a guaranteed property. Adjust the formatting of v1 error strings to better match how they were historically rendered. This does not ensure 100% equivalency with legacy error strings, but does a better job at mostly preserving the exact string. In the jsontext and jsonwire packages, we make changes to assist in the construction legacy error values. We also make syntactic errors returned by the Decoder more consistent regardless of whether they were produced by a ReadValue or ReadToken call. All changes made to jsontext and jsonwire are an improvement, so we are not taking on technical debt in those packages to support legacy error strings. All legacy transformations are kept in the v1 code. --- arshal_test.go | 12 ++-- internal/jsonwire/decode.go | 12 ++-- internal/jsonwire/decode_test.go | 42 ++++++------- internal/jsonwire/wire.go | 4 +- jsontext/decode.go | 22 ++++++- jsontext/decode_test.go | 100 +++++++++++++++++-------------- jsontext/encode_test.go | 10 ++-- jsontext/errors.go | 14 +++++ jsontext/state.go | 17 ------ jsontext/token.go | 6 +- jsontext/token_test.go | 2 +- jsontext/value.go | 6 +- migrate.sh | 5 -- v1/decode_test.go | 54 ++++++++--------- v1/inject.go | 32 +++++++++- v1/scanner.go | 19 +++++- v1/scanner_test.go | 4 +- v1/stream.go | 5 ++ v1/stream_test.go | 8 +-- 19 files changed, 220 insertions(+), 154 deletions(-) diff --git a/arshal_test.go b/arshal_test.go index 09d865a..31e932b 100644 --- a/arshal_test.go +++ b/arshal_test.go @@ -2531,7 +2531,7 @@ func TestMarshal(t *testing.T) { name: jsontest.Name("Structs/InlinedFallback/TextValue/InvalidObjectEnd"), in: structInlineTextValue{X: jsontext.Value(` { "name" : false , } `)}, want: `{"name":false`, - wantErr: EM(newInvalidCharacterError(",", "before next token", len64(` { "name" : false `), "")).withPos(`{"name":false,`, "").withType(0, T[jsontext.Value]()), + wantErr: EM(newInvalidCharacterError(",", "at start of value", len64(` { "name" : false `), "")).withPos(`{"name":false,`, "").withType(0, T[jsontext.Value]()), }, { name: jsontest.Name("Structs/InlinedFallback/TextValue/InvalidDualObject"), in: structInlineTextValue{X: jsontext.Value(`{}{}`)}, @@ -6626,7 +6626,7 @@ func TestUnmarshal(t *testing.T) { inBuf: `{"A":1,"fizz":nil,"B":2}`, inVal: new(structInlineTextValue), want: addr(structInlineTextValue{A: 1, X: jsontext.Value(`{"fizz":`)}), - wantErr: newInvalidCharacterError("i", "within literal null (expecting 'u')", len64(`{"A":1,"fizz":n`), "/fizz"), + wantErr: newInvalidCharacterError("i", "in literal null (expecting 'u')", len64(`{"A":1,"fizz":n`), "/fizz"), }, { name: jsontest.Name("Structs/InlinedFallback/TextValue/CaseSensitive"), inBuf: `{"A":1,"fizz":"buzz","B":2,"a":3}`, @@ -6736,13 +6736,13 @@ func TestUnmarshal(t *testing.T) { inBuf: `{"A":1,"fizz":nil,"B":2}`, inVal: new(structInlineMapStringAny), want: addr(structInlineMapStringAny{A: 1, X: jsonObject{"fizz": nil}}), - wantErr: newInvalidCharacterError("i", "within literal null (expecting 'u')", len64(`{"A":1,"fizz":n`), "/fizz"), + wantErr: newInvalidCharacterError("i", "in literal null (expecting 'u')", len64(`{"A":1,"fizz":n`), "/fizz"), }, { name: jsontest.Name("Structs/InlinedFallback/MapStringAny/MergeInvalidValue/Existing"), inBuf: `{"A":1,"fizz":nil,"B":2}`, inVal: addr(structInlineMapStringAny{A: 1, X: jsonObject{"fizz": true}}), want: addr(structInlineMapStringAny{A: 1, X: jsonObject{"fizz": true}}), - wantErr: newInvalidCharacterError("i", "within literal null (expecting 'u')", len64(`{"A":1,"fizz":n`), "/fizz"), + wantErr: newInvalidCharacterError("i", "in literal null (expecting 'u')", len64(`{"A":1,"fizz":n`), "/fizz"), }, { name: jsontest.Name("Structs/InlinedFallback/MapStringAny/CaseSensitive"), inBuf: `{"A":1,"fizz":"buzz","B":2,"a":3}`, @@ -6959,13 +6959,13 @@ func TestUnmarshal(t *testing.T) { inBuf: `{"A":1,"fizz":nil,"B":2}`, inVal: new(structInlineMapNamedStringAny), want: addr(structInlineMapNamedStringAny{A: 1, X: map[namedString]any{"fizz": nil}}), - wantErr: newInvalidCharacterError("i", "within literal null (expecting 'u')", len64(`{"A":1,"fizz":n`), "/fizz"), + wantErr: newInvalidCharacterError("i", "in literal null (expecting 'u')", len64(`{"A":1,"fizz":n`), "/fizz"), }, { name: jsontest.Name("Structs/InlinedFallback/MapNamedStringAny/MergeInvalidValue/Existing"), inBuf: `{"A":1,"fizz":nil,"B":2}`, inVal: addr(structInlineMapNamedStringAny{A: 1, X: map[namedString]any{"fizz": true}}), want: addr(structInlineMapNamedStringAny{A: 1, X: map[namedString]any{"fizz": true}}), - wantErr: newInvalidCharacterError("i", "within literal null (expecting 'u')", len64(`{"A":1,"fizz":n`), "/fizz"), + wantErr: newInvalidCharacterError("i", "in literal null (expecting 'u')", len64(`{"A":1,"fizz":n`), "/fizz"), }, { name: jsontest.Name("Structs/InlinedFallback/MapNamedStringAny/CaseSensitive"), inBuf: `{"A":1,"fizz":"buzz","B":2,"a":3}`, diff --git a/internal/jsonwire/decode.go b/internal/jsonwire/decode.go index 03eb8e0..4b9d5a6 100644 --- a/internal/jsonwire/decode.go +++ b/internal/jsonwire/decode.go @@ -74,7 +74,7 @@ func ConsumeTrue(b []byte) int { func ConsumeLiteral(b []byte, lit string) (n int, err error) { for i := 0; i < len(b) && i < len(lit); i++ { if b[i] != lit[i] { - return i, NewInvalidCharacterError(b[i:], "within literal "+lit+" (expecting "+strconv.QuoteRune(rune(lit[i]))+")") + return i, NewInvalidCharacterError(b[i:], "in literal "+lit+" (expecting "+strconv.QuoteRune(rune(lit[i]))+")") } } if len(b) < len(lit) { @@ -240,7 +240,7 @@ func ConsumeStringResumable(flags *ValueFlags, b []byte, resumeOffset int, valid // Handle invalid control characters. case r < ' ': flags.Join(stringNonVerbatim | stringNonCanonical) - return n, NewInvalidCharacterError(b[n:], "within string (expecting non-control character)") + return n, NewInvalidCharacterError(b[n:], "in string (expecting non-control character)") default: panic("BUG: unhandled character " + QuoteRune(b[n:])) } @@ -374,7 +374,7 @@ func AppendUnquote[Bytes ~[]byte | ~string](dst []byte, src Bytes) (v []byte, er // Handle invalid control characters. case r < ' ': dst = append(dst, src[i:n]...) - return dst, NewInvalidCharacterError(src[n:], "within string (expecting non-control character)") + return dst, NewInvalidCharacterError(src[n:], "in string (expecting non-control character)") default: panic("BUG: unhandled character " + QuoteRune(src[n:])) } @@ -513,7 +513,7 @@ beforeInteger: } state = withinIntegerDigits default: - return n, state, NewInvalidCharacterError(b[n:], "within number (expecting digit)") + return n, state, NewInvalidCharacterError(b[n:], "in number (expecting digit)") } // Consume optional fractional component. @@ -527,7 +527,7 @@ beforeFractional: case '0' <= b[n] && b[n] <= '9': n++ default: - return n, state, NewInvalidCharacterError(b[n:], "within number (expecting digit)") + return n, state, NewInvalidCharacterError(b[n:], "in number (expecting digit)") } for uint(len(b)) > uint(n) && ('0' <= b[n] && b[n] <= '9') { n++ @@ -549,7 +549,7 @@ beforeExponent: case '0' <= b[n] && b[n] <= '9': n++ default: - return n, state, NewInvalidCharacterError(b[n:], "within number (expecting digit)") + return n, state, NewInvalidCharacterError(b[n:], "in number (expecting digit)") } for uint(len(b)) > uint(n) && ('0' <= b[n] && b[n] <= '9') { n++ diff --git a/internal/jsonwire/decode_test.go b/internal/jsonwire/decode_test.go index c4b7ff0..1748b59 100644 --- a/internal/jsonwire/decode_test.go +++ b/internal/jsonwire/decode_test.go @@ -49,8 +49,8 @@ func TestConsumeLiteral(t *testing.T) { {"null", "nul", 3, io.ErrUnexpectedEOF}, {"null", "null", 4, nil}, {"null", "nullx", 4, nil}, - {"null", "x", 0, NewInvalidCharacterError("x", "within literal null (expecting 'n')")}, - {"null", "nuxx", 2, NewInvalidCharacterError("x", "within literal null (expecting 'l')")}, + {"null", "x", 0, NewInvalidCharacterError("x", "in literal null (expecting 'n')")}, + {"null", "nuxx", 2, NewInvalidCharacterError("x", "in literal null (expecting 'l')")}, {"false", "", 0, io.ErrUnexpectedEOF}, {"false", "f", 1, io.ErrUnexpectedEOF}, @@ -59,8 +59,8 @@ func TestConsumeLiteral(t *testing.T) { {"false", "fals", 4, io.ErrUnexpectedEOF}, {"false", "false", 5, nil}, {"false", "falsex", 5, nil}, - {"false", "x", 0, NewInvalidCharacterError("x", "within literal false (expecting 'f')")}, - {"false", "falsx", 4, NewInvalidCharacterError("x", "within literal false (expecting 'e')")}, + {"false", "x", 0, NewInvalidCharacterError("x", "in literal false (expecting 'f')")}, + {"false", "falsx", 4, NewInvalidCharacterError("x", "in literal false (expecting 'e')")}, {"true", "", 0, io.ErrUnexpectedEOF}, {"true", "t", 1, io.ErrUnexpectedEOF}, @@ -68,8 +68,8 @@ func TestConsumeLiteral(t *testing.T) { {"true", "tru", 3, io.ErrUnexpectedEOF}, {"true", "true", 4, nil}, {"true", "truex", 4, nil}, - {"true", "x", 0, NewInvalidCharacterError("x", "within literal true (expecting 't')")}, - {"true", "trux", 3, NewInvalidCharacterError("x", "within literal true (expecting 'e')")}, + {"true", "x", 0, NewInvalidCharacterError("x", "in literal true (expecting 't')")}, + {"true", "trux", 3, NewInvalidCharacterError("x", "in literal true (expecting 'e')")}, } for _, tt := range tests { @@ -120,9 +120,9 @@ func TestConsumeString(t *testing.T) { {` ""x`, false, 0, 0, 0, "", NewInvalidCharacterError(" ", "at start of string (expecting '\"')"), errPrev, errPrev}, {`"hello`, false, 6, 6, 0, "hello", io.ErrUnexpectedEOF, errPrev, errPrev}, {`"hello"`, true, 7, 7, 0, "hello", nil, nil, nil}, - {"\"\x00\"", false, 1, 1, stringNonVerbatim | stringNonCanonical, "", NewInvalidCharacterError("\x00", "within string (expecting non-control character)"), errPrev, errPrev}, + {"\"\x00\"", false, 1, 1, stringNonVerbatim | stringNonCanonical, "", NewInvalidCharacterError("\x00", "in string (expecting non-control character)"), errPrev, errPrev}, {`"\u0000"`, false, 8, 8, stringNonVerbatim, "\x00", nil, nil, nil}, - {"\"\x1f\"", false, 1, 1, stringNonVerbatim | stringNonCanonical, "", NewInvalidCharacterError("\x1f", "within string (expecting non-control character)"), errPrev, errPrev}, + {"\"\x1f\"", false, 1, 1, stringNonVerbatim | stringNonCanonical, "", NewInvalidCharacterError("\x1f", "in string (expecting non-control character)"), errPrev, errPrev}, {`"\u001f"`, false, 8, 8, stringNonVerbatim, "\x1f", nil, nil, nil}, {`"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"`, true, 54, 54, 0, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", nil, nil, nil}, {"\" !#$%'()*+,-./0123456789:;=?@[]^_`{|}~\x7f\"", true, 41, 41, 0, " !#$%'()*+,-./0123456789:;=?@[]^_`{|}~\x7f", nil, nil, nil}, @@ -227,13 +227,13 @@ func TestConsumeNumber(t *testing.T) { wantErr error }{ {"", false, 0, io.ErrUnexpectedEOF}, - {`"NaN"`, false, 0, NewInvalidCharacterError("\"", "within number (expecting digit)")}, - {`"Infinity"`, false, 0, NewInvalidCharacterError("\"", "within number (expecting digit)")}, - {`"-Infinity"`, false, 0, NewInvalidCharacterError("\"", "within number (expecting digit)")}, - {".0", false, 0, NewInvalidCharacterError(".", "within number (expecting digit)")}, + {`"NaN"`, false, 0, NewInvalidCharacterError("\"", "in number (expecting digit)")}, + {`"Infinity"`, false, 0, NewInvalidCharacterError("\"", "in number (expecting digit)")}, + {`"-Infinity"`, false, 0, NewInvalidCharacterError("\"", "in number (expecting digit)")}, + {".0", false, 0, NewInvalidCharacterError(".", "in number (expecting digit)")}, {"0", true, 1, nil}, {"-0", false, 2, nil}, - {"+0", false, 0, NewInvalidCharacterError("+", "within number (expecting digit)")}, + {"+0", false, 0, NewInvalidCharacterError("+", "in number (expecting digit)")}, {"1", true, 1, nil}, {"-1", false, 2, nil}, {"00", true, 1, nil}, @@ -248,8 +248,8 @@ func TestConsumeNumber(t *testing.T) { {"-9876543210", false, 11, nil}, {"9876543210x", true, 10, nil}, {"-9876543210x", false, 11, nil}, - {" 9876543210", true, 0, NewInvalidCharacterError(" ", "within number (expecting digit)")}, - {"- 9876543210", false, 1, NewInvalidCharacterError(" ", "within number (expecting digit)")}, + {" 9876543210", true, 0, NewInvalidCharacterError(" ", "in number (expecting digit)")}, + {"- 9876543210", false, 1, NewInvalidCharacterError(" ", "in number (expecting digit)")}, {strings.Repeat("9876543210", 1000), true, 10000, nil}, {"-" + strings.Repeat("9876543210", 1000), false, 1 + 10000, nil}, {"0.", false, 1, io.ErrUnexpectedEOF}, @@ -266,16 +266,16 @@ func TestConsumeNumber(t *testing.T) { {"-0E0", false, 4, nil}, {"0.0123456789", false, 12, nil}, {"-0.0123456789", false, 13, nil}, - {"1.f", false, 2, NewInvalidCharacterError("f", "within number (expecting digit)")}, - {"-1.f", false, 3, NewInvalidCharacterError("f", "within number (expecting digit)")}, - {"1.e", false, 2, NewInvalidCharacterError("e", "within number (expecting digit)")}, - {"-1.e", false, 3, NewInvalidCharacterError("e", "within number (expecting digit)")}, + {"1.f", false, 2, NewInvalidCharacterError("f", "in number (expecting digit)")}, + {"-1.f", false, 3, NewInvalidCharacterError("f", "in number (expecting digit)")}, + {"1.e", false, 2, NewInvalidCharacterError("e", "in number (expecting digit)")}, + {"-1.e", false, 3, NewInvalidCharacterError("e", "in number (expecting digit)")}, {"1e0", false, 3, nil}, {"-1e0", false, 4, nil}, {"1E0", false, 3, nil}, {"-1E0", false, 4, nil}, - {"1Ex", false, 2, NewInvalidCharacterError("x", "within number (expecting digit)")}, - {"-1Ex", false, 3, NewInvalidCharacterError("x", "within number (expecting digit)")}, + {"1Ex", false, 2, NewInvalidCharacterError("x", "in number (expecting digit)")}, + {"-1Ex", false, 3, NewInvalidCharacterError("x", "in number (expecting digit)")}, {"1e-0", false, 4, nil}, {"-1e-0", false, 5, nil}, {"1e+0", false, 4, nil}, diff --git a/internal/jsonwire/wire.go b/internal/jsonwire/wire.go index 6632382..d9ddc29 100644 --- a/internal/jsonwire/wire.go +++ b/internal/jsonwire/wire.go @@ -157,9 +157,9 @@ func NewInvalidEscapeSequenceError[Bytes ~[]byte | ~string](what Bytes) error { return r == '`' || r == utf8.RuneError || unicode.IsSpace(r) || !unicode.IsPrint(r) }) >= 0 if needEscape { - return errors.New("invalid " + label + " " + strconv.Quote(string(what)) + " within string") + return errors.New("invalid " + label + " " + strconv.Quote(string(what)) + " in string") } else { - return errors.New("invalid " + label + " `" + string(what) + "` within string") + return errors.New("invalid " + label + " `" + string(what) + "` in string") } } diff --git a/jsontext/decode.go b/jsontext/decode.go index ee6cc3f..6f14095 100644 --- a/jsontext/decode.go +++ b/jsontext/decode.go @@ -364,9 +364,22 @@ func (d *decoderState) CountNextDelimWhitespace() int { // checkDelim checks whether delim is valid for the given next kind. func (d *decoderState) checkDelim(delim byte, next Kind) error { + where := "at start of value" + switch d.Tokens.needDelim(next) { + case delim: + return nil + case ':': + where = "after object name (expecting ':')" + case ',': + if d.Tokens.Last.isObject() { + where = "after object value (expecting ',' or '}')" + } else { + where = "after array element (expecting ',' or ']')" + } + } pos := d.prevEnd // restore position to right after leading whitespace pos += jsonwire.ConsumeWhitespace(d.buf[pos:]) - err := d.Tokens.checkDelim(delim, next) + err := jsonwire.NewInvalidCharacterError(d.buf[pos:], where) return wrapSyntacticError(d, err, pos, 0) } @@ -618,7 +631,7 @@ func (d *decoderState) ReadToken() (Token, error) { return ArrayEnd, nil default: - err = jsonwire.NewInvalidCharacterError(d.buf[pos:], "at start of token") + err = jsonwire.NewInvalidCharacterError(d.buf[pos:], "at start of value") return Token{}, wrapSyntacticError(d, err, pos, +1) } } @@ -833,6 +846,9 @@ func (d *decoderState) consumeValue(flags *jsonwire.ValueFlags, pos, depth int) case '[': return d.consumeArray(flags, pos, depth) default: + if (d.Tokens.Last.isObject() && next == ']') || (d.Tokens.Last.isArray() && next == '}') { + return pos, errMismatchDelim + } return pos, jsonwire.NewInvalidCharacterError(d.buf[pos:], "at start of value") } if err == io.ErrUnexpectedEOF { @@ -1068,7 +1084,7 @@ func (d *decoderState) consumeArray(flags *jsonwire.ValueFlags, pos, depth int) pos++ return pos, nil default: - return pos, jsonwire.NewInvalidCharacterError(d.buf[pos:], "after array value (expecting ',' or ']')") + return pos, jsonwire.NewInvalidCharacterError(d.buf[pos:], "after array element (expecting ',' or ']')") } } } diff --git a/jsontext/decode_test.go b/jsontext/decode_test.go index b7e7efd..80f235d 100644 --- a/jsontext/decode_test.go +++ b/jsontext/decode_test.go @@ -191,7 +191,7 @@ var decoderErrorTestdata = []struct { name: jsontest.Name("InvalidStart"), in: ` #`, calls: []decoderMethodCall{ - {'#', zeroToken, newInvalidCharacterError("#", "at start of token").withPos(" ", ""), ""}, + {'#', zeroToken, newInvalidCharacterError("#", "at start of value").withPos(" ", ""), ""}, {'#', zeroValue, newInvalidCharacterError("#", "at start of value").withPos(" ", ""), ""}, }, }, { @@ -225,8 +225,8 @@ var decoderErrorTestdata = []struct { in: ` null , null `, calls: []decoderMethodCall{ {'n', Null, nil, ""}, - {0, zeroToken, newInvalidCharacterError(",", `before next token`).withPos(` null `, ""), ""}, - {0, zeroValue, newInvalidCharacterError(",", `before next token`).withPos(` null `, ""), ""}, + {0, zeroToken, newInvalidCharacterError(",", `at start of value`).withPos(` null `, ""), ""}, + {0, zeroValue, newInvalidCharacterError(",", `at start of value`).withPos(` null `, ""), ""}, }, wantOffset: len(` null`), }, { @@ -240,8 +240,8 @@ var decoderErrorTestdata = []struct { name: jsontest.Name("InvalidNull"), in: `nulL`, calls: []decoderMethodCall{ - {'n', zeroToken, newInvalidCharacterError("L", `within literal null (expecting 'l')`).withPos(`nul`, ""), ""}, - {'n', zeroValue, newInvalidCharacterError("L", `within literal null (expecting 'l')`).withPos(`nul`, ""), ""}, + {'n', zeroToken, newInvalidCharacterError("L", `in literal null (expecting 'l')`).withPos(`nul`, ""), ""}, + {'n', zeroValue, newInvalidCharacterError("L", `in literal null (expecting 'l')`).withPos(`nul`, ""), ""}, }, }, { name: jsontest.Name("TruncatedFalse"), @@ -254,8 +254,8 @@ var decoderErrorTestdata = []struct { name: jsontest.Name("InvalidFalse"), in: `falsE`, calls: []decoderMethodCall{ - {'f', zeroToken, newInvalidCharacterError("E", `within literal false (expecting 'e')`).withPos(`fals`, ""), ""}, - {'f', zeroValue, newInvalidCharacterError("E", `within literal false (expecting 'e')`).withPos(`fals`, ""), ""}, + {'f', zeroToken, newInvalidCharacterError("E", `in literal false (expecting 'e')`).withPos(`fals`, ""), ""}, + {'f', zeroValue, newInvalidCharacterError("E", `in literal false (expecting 'e')`).withPos(`fals`, ""), ""}, }, }, { name: jsontest.Name("TruncatedTrue"), @@ -268,8 +268,8 @@ var decoderErrorTestdata = []struct { name: jsontest.Name("InvalidTrue"), in: `truE`, calls: []decoderMethodCall{ - {'t', zeroToken, newInvalidCharacterError("E", `within literal true (expecting 'e')`).withPos(`tru`, ""), ""}, - {'t', zeroValue, newInvalidCharacterError("E", `within literal true (expecting 'e')`).withPos(`tru`, ""), ""}, + {'t', zeroToken, newInvalidCharacterError("E", `in literal true (expecting 'e')`).withPos(`tru`, ""), ""}, + {'t', zeroValue, newInvalidCharacterError("E", `in literal true (expecting 'e')`).withPos(`tru`, ""), ""}, }, }, { name: jsontest.Name("TruncatedString"), @@ -282,8 +282,8 @@ var decoderErrorTestdata = []struct { name: jsontest.Name("InvalidString"), in: `"ok` + "\x00", calls: []decoderMethodCall{ - {'"', zeroToken, newInvalidCharacterError("\x00", `within string (expecting non-control character)`).withPos(`"ok`, ""), ""}, - {'"', zeroValue, newInvalidCharacterError("\x00", `within string (expecting non-control character)`).withPos(`"ok`, ""), ""}, + {'"', zeroToken, newInvalidCharacterError("\x00", `in string (expecting non-control character)`).withPos(`"ok`, ""), ""}, + {'"', zeroValue, newInvalidCharacterError("\x00", `in string (expecting non-control character)`).withPos(`"ok`, ""), ""}, }, }, { name: jsontest.Name("ValidString/AllowInvalidUTF8/Token"), @@ -320,8 +320,8 @@ var decoderErrorTestdata = []struct { name: jsontest.Name("InvalidNumber"), in: `0.e`, calls: []decoderMethodCall{ - {'0', zeroToken, newInvalidCharacterError("e", "within number (expecting digit)").withPos(`0.`, ""), ""}, - {'0', zeroValue, newInvalidCharacterError("e", "within number (expecting digit)").withPos(`0.`, ""), ""}, + {'0', zeroToken, newInvalidCharacterError("e", "in number (expecting digit)").withPos(`0.`, ""), ""}, + {'0', zeroValue, newInvalidCharacterError("e", "in number (expecting digit)").withPos(`0.`, ""), ""}, }, }, { name: jsontest.Name("TruncatedObject/AfterStart"), @@ -386,8 +386,8 @@ var decoderErrorTestdata = []struct { {'{', zeroValue, newInvalidCharacterError("\"", "after object name (expecting ':')").withPos(` { "fizz" `, "/fizz"), ""}, {'{', ObjectStart, nil, ""}, {'"', String("fizz"), nil, ""}, - {0, zeroToken, E(errMissingColon).withPos(` { "fizz" `, "/fizz"), ""}, - {0, zeroValue, E(errMissingColon).withPos(` { "fizz" `, "/fizz"), ""}, + {0, zeroToken, newInvalidCharacterError("\"", "after object name (expecting ':')").withPos(` { "fizz" `, "/fizz"), ""}, + {0, zeroValue, newInvalidCharacterError("\"", "after object name (expecting ':')").withPos(` { "fizz" `, "/fizz"), ""}, }, wantOffset: len(` { "fizz"`), }, { @@ -397,8 +397,8 @@ var decoderErrorTestdata = []struct { {'{', zeroValue, newInvalidCharacterError(",", "after object name (expecting ':')").withPos(` { "fizz" `, "/fizz"), ""}, {'{', ObjectStart, nil, ""}, {'"', String("fizz"), nil, ""}, - {0, zeroToken, E(errMissingColon).withPos(` { "fizz" `, "/fizz"), ""}, - {0, zeroValue, E(errMissingColon).withPos(` { "fizz" `, "/fizz"), ""}, + {0, zeroToken, newInvalidCharacterError(",", "after object name (expecting ':')").withPos(` { "fizz" `, "/fizz"), ""}, + {0, zeroValue, newInvalidCharacterError(",", "after object name (expecting ':')").withPos(` { "fizz" `, "/fizz"), ""}, }, wantOffset: len(` { "fizz"`), }, { @@ -408,8 +408,8 @@ var decoderErrorTestdata = []struct { {'{', zeroValue, newInvalidCharacterError("#", "after object name (expecting ':')").withPos(` { "fizz" `, "/fizz"), ""}, {'{', ObjectStart, nil, ""}, {'"', String("fizz"), nil, ""}, - {0, zeroToken, E(errMissingColon).withPos(` { "fizz" `, "/fizz"), ""}, - {0, zeroValue, E(errMissingColon).withPos(` { "fizz" `, "/fizz"), ""}, + {0, zeroToken, newInvalidCharacterError("#", "after object name (expecting ':')").withPos(` { "fizz" `, "/fizz"), ""}, + {0, zeroValue, newInvalidCharacterError("#", "after object name (expecting ':')").withPos(` { "fizz" `, "/fizz"), ""}, }, wantOffset: len(` { "fizz"`), }, { @@ -420,8 +420,8 @@ var decoderErrorTestdata = []struct { {'{', ObjectStart, nil, ""}, {'"', String("fizz"), nil, ""}, {'"', String("buzz"), nil, ""}, - {0, zeroToken, E(errMissingComma).withPos(` { "fizz" : "buzz" `, ""), ""}, - {0, zeroValue, E(errMissingComma).withPos(` { "fizz" : "buzz" `, ""), ""}, + {0, zeroToken, newInvalidCharacterError("\"", "after object value (expecting ',' or '}')").withPos(` { "fizz" : "buzz" `, ""), ""}, + {0, zeroValue, newInvalidCharacterError("\"", "after object value (expecting ',' or '}')").withPos(` { "fizz" : "buzz" `, ""), ""}, }, wantOffset: len(` { "fizz" : "buzz"`), }, { @@ -432,8 +432,8 @@ var decoderErrorTestdata = []struct { {'{', ObjectStart, nil, ""}, {'"', String("fizz"), nil, ""}, {'"', String("buzz"), nil, ""}, - {0, zeroToken, E(errMissingComma).withPos(` { "fizz" : "buzz" `, ""), ""}, - {0, zeroValue, E(errMissingComma).withPos(` { "fizz" : "buzz" `, ""), ""}, + {0, zeroToken, newInvalidCharacterError(":", "after object value (expecting ',' or '}')").withPos(` { "fizz" : "buzz" `, ""), ""}, + {0, zeroValue, newInvalidCharacterError(":", "after object value (expecting ',' or '}')").withPos(` { "fizz" : "buzz" `, ""), ""}, }, wantOffset: len(` { "fizz" : "buzz"`), }, { @@ -444,8 +444,8 @@ var decoderErrorTestdata = []struct { {'{', ObjectStart, nil, ""}, {'"', String("fizz"), nil, ""}, {'"', String("buzz"), nil, ""}, - {0, zeroToken, E(errMissingComma).withPos(` { "fizz" : "buzz" `, ""), ""}, - {0, zeroValue, E(errMissingComma).withPos(` { "fizz" : "buzz" `, ""), ""}, + {0, zeroToken, newInvalidCharacterError("#", "after object value (expecting ',' or '}')").withPos(` { "fizz" : "buzz" `, ""), ""}, + {0, zeroValue, newInvalidCharacterError("#", "after object value (expecting ',' or '}')").withPos(` { "fizz" : "buzz" `, ""), ""}, }, wantOffset: len(` { "fizz" : "buzz"`), }, { @@ -454,8 +454,8 @@ var decoderErrorTestdata = []struct { calls: []decoderMethodCall{ {'{', zeroValue, newInvalidCharacterError(",", `at start of string (expecting '"')`).withPos(` { `, ""), ""}, {'{', ObjectStart, nil, ""}, - {0, zeroToken, newInvalidCharacterError(",", `before next token`).withPos(` { `, ""), ""}, - {0, zeroValue, newInvalidCharacterError(",", `before next token`).withPos(` { `, ""), ""}, + {0, zeroToken, newInvalidCharacterError(",", `at start of value`).withPos(` { `, ""), ""}, + {0, zeroValue, newInvalidCharacterError(",", `at start of value`).withPos(` { `, ""), ""}, }, wantOffset: len(` {`), }, { @@ -466,8 +466,8 @@ var decoderErrorTestdata = []struct { {'{', ObjectStart, nil, ""}, {'"', String("fizz"), nil, ""}, {'"', String("buzz"), nil, ""}, - {0, zeroToken, newInvalidCharacterError(",", `before next token`).withPos(` { "fizz" : "buzz" `, ""), ""}, - {0, zeroValue, newInvalidCharacterError(",", `before next token`).withPos(` { "fizz" : "buzz" `, ""), ""}, + {0, zeroToken, newInvalidCharacterError(",", `at start of value`).withPos(` { "fizz" : "buzz" `, ""), ""}, + {0, zeroValue, newInvalidCharacterError(",", `at start of value`).withPos(` { "fizz" : "buzz" `, ""), ""}, }, wantOffset: len(` { "fizz" : "buzz"`), }, { @@ -536,7 +536,7 @@ var decoderErrorTestdata = []struct { calls: []decoderMethodCall{ {'{', zeroValue, newInvalidCharacterError("]", "at start of string (expecting '\"')").withPos(` { `, ""), ""}, {'{', ObjectStart, nil, ""}, - {']', zeroToken, E(errMismatchDelim).withPos(` { `, ""), ""}, + {']', zeroToken, newInvalidCharacterError("]", "at start of value").withPos(` { `, ""), ""}, {']', zeroValue, newInvalidCharacterError("]", "at start of value").withPos(` { `, ""), ""}, }, wantOffset: len(` {`), @@ -625,11 +625,11 @@ var decoderErrorTestdata = []struct { name: jsontest.Name("InvalidArray/MissingComma"), in: ` [ "fizz" "buzz" ] `, calls: []decoderMethodCall{ - {'[', zeroValue, newInvalidCharacterError("\"", "after array value (expecting ',' or ']')").withPos(` [ "fizz" `, ""), ""}, + {'[', zeroValue, newInvalidCharacterError("\"", "after array element (expecting ',' or ']')").withPos(` [ "fizz" `, ""), ""}, {'[', ArrayStart, nil, ""}, {'"', String("fizz"), nil, ""}, - {0, zeroToken, E(errMissingComma).withPos(` [ "fizz" `, ""), ""}, - {0, zeroValue, E(errMissingComma).withPos(` [ "fizz" `, ""), ""}, + {0, zeroToken, newInvalidCharacterError("\"", "after array element (expecting ',' or ']')").withPos(` [ "fizz" `, ""), ""}, + {0, zeroValue, newInvalidCharacterError("\"", "after array element (expecting ',' or ']')").withPos(` [ "fizz" `, ""), ""}, }, wantOffset: len(` [ "fizz"`), }, { @@ -638,7 +638,7 @@ var decoderErrorTestdata = []struct { calls: []decoderMethodCall{ {'[', zeroValue, newInvalidCharacterError("}", "at start of value").withPos(` [ `, "/0"), ""}, {'[', ArrayStart, nil, ""}, - {'}', zeroToken, E(errMismatchDelim).withPos(` [ `, "/0"), ""}, + {'}', zeroToken, newInvalidCharacterError("}", "at start of value").withPos(` [ `, "/0"), ""}, {'}', zeroValue, newInvalidCharacterError("}", "at start of value").withPos(` [ `, "/0"), ""}, }, wantOffset: len(` [`), @@ -655,33 +655,36 @@ var decoderErrorTestdata = []struct { in: `"",`, calls: []decoderMethodCall{ {'"', String(""), nil, ""}, - {0, zeroToken, newInvalidCharacterError(",", "before next token").withPos(`""`, ""), ""}, - {0, zeroValue, newInvalidCharacterError(",", "before next token").withPos(`""`, ""), ""}, + {0, zeroToken, newInvalidCharacterError(",", "at start of value").withPos(`""`, ""), ""}, + {0, zeroValue, newInvalidCharacterError(",", "at start of value").withPos(`""`, ""), ""}, }, wantOffset: len(`""`), }, { name: jsontest.Name("InvalidDelim/AfterObjectStart"), in: `{:`, calls: []decoderMethodCall{ + {'{', zeroValue, newInvalidCharacterError(":", `at start of string (expecting '"')`).withPos(`{`, ""), ""}, {'{', ObjectStart, nil, ""}, - {0, zeroToken, newInvalidCharacterError(":", "before next token").withPos(`{`, ""), ""}, - {0, zeroValue, newInvalidCharacterError(":", "before next token").withPos(`{`, ""), ""}, + {0, zeroToken, newInvalidCharacterError(":", "at start of value").withPos(`{`, ""), ""}, + {0, zeroValue, newInvalidCharacterError(":", "at start of value").withPos(`{`, ""), ""}, }, wantOffset: len(`{`), }, { name: jsontest.Name("InvalidDelim/AfterObjectName"), in: `{"",`, calls: []decoderMethodCall{ + {'{', zeroValue, newInvalidCharacterError(",", "after object name (expecting ':')").withPos(`{""`, "/"), ""}, {'{', ObjectStart, nil, ""}, {'"', String(""), nil, ""}, - {0, zeroToken, E(errMissingColon).withPos(`{""`, "/"), ""}, - {0, zeroValue, E(errMissingColon).withPos(`{""`, "/"), ""}, + {0, zeroToken, newInvalidCharacterError(",", "after object name (expecting ':')").withPos(`{""`, "/"), ""}, + {0, zeroValue, newInvalidCharacterError(",", "after object name (expecting ':')").withPos(`{""`, "/"), ""}, }, wantOffset: len(`{""`), }, { name: jsontest.Name("ValidDelim/AfterObjectName"), in: `{"":`, calls: []decoderMethodCall{ + {'{', zeroValue, E(io.ErrUnexpectedEOF).withPos(`{"":`, "/"), ""}, {'{', ObjectStart, nil, ""}, {'"', String(""), nil, ""}, {0, zeroToken, E(io.ErrUnexpectedEOF).withPos(`{"":`, "/"), ""}, @@ -692,17 +695,19 @@ var decoderErrorTestdata = []struct { name: jsontest.Name("InvalidDelim/AfterObjectValue"), in: `{"":"":`, calls: []decoderMethodCall{ + {'{', zeroValue, newInvalidCharacterError(":", "after object value (expecting ',' or '}')").withPos(`{"":""`, ""), ""}, {'{', ObjectStart, nil, ""}, {'"', String(""), nil, ""}, {'"', String(""), nil, ""}, - {0, zeroToken, E(errMissingComma).withPos(`{"":""`, ""), ""}, - {0, zeroValue, E(errMissingComma).withPos(`{"":""`, ""), ""}, + {0, zeroToken, newInvalidCharacterError(":", "after object value (expecting ',' or '}')").withPos(`{"":""`, ""), ""}, + {0, zeroValue, newInvalidCharacterError(":", "after object value (expecting ',' or '}')").withPos(`{"":""`, ""), ""}, }, wantOffset: len(`{"":""`), }, { name: jsontest.Name("ValidDelim/AfterObjectValue"), in: `{"":"",`, calls: []decoderMethodCall{ + {'{', zeroValue, E(io.ErrUnexpectedEOF).withPos(`{"":"",`, ""), ""}, {'{', ObjectStart, nil, ""}, {'"', String(""), nil, ""}, {'"', String(""), nil, ""}, @@ -714,25 +719,28 @@ var decoderErrorTestdata = []struct { name: jsontest.Name("InvalidDelim/AfterArrayStart"), in: `[,`, calls: []decoderMethodCall{ + {'[', zeroValue, newInvalidCharacterError(",", "at start of value").withPos(`[`, "/0"), ""}, {'[', ArrayStart, nil, ""}, - {0, zeroToken, newInvalidCharacterError(",", "before next token").withPos(`[`, ""), ""}, - {0, zeroValue, newInvalidCharacterError(",", "before next token").withPos(`[`, ""), ""}, + {0, zeroToken, newInvalidCharacterError(",", "at start of value").withPos(`[`, ""), ""}, + {0, zeroValue, newInvalidCharacterError(",", "at start of value").withPos(`[`, ""), ""}, }, wantOffset: len(`[`), }, { name: jsontest.Name("InvalidDelim/AfterArrayValue"), in: `["":`, calls: []decoderMethodCall{ + {'[', zeroValue, newInvalidCharacterError(":", "after array element (expecting ',' or ']')").withPos(`[""`, ""), ""}, {'[', ArrayStart, nil, ""}, {'"', String(""), nil, ""}, - {0, zeroToken, E(errMissingComma).withPos(`[""`, ""), ""}, - {0, zeroValue, E(errMissingComma).withPos(`[""`, ""), ""}, + {0, zeroToken, newInvalidCharacterError(":", "after array element (expecting ',' or ']')").withPos(`[""`, ""), ""}, + {0, zeroValue, newInvalidCharacterError(":", "after array element (expecting ',' or ']')").withPos(`[""`, ""), ""}, }, wantOffset: len(`[""`), }, { name: jsontest.Name("ValidDelim/AfterArrayValue"), in: `["",`, calls: []decoderMethodCall{ + {'[', zeroValue, E(io.ErrUnexpectedEOF).withPos(`["",`, ""), ""}, {'[', ArrayStart, nil, ""}, {'"', String(""), nil, ""}, {0, zeroToken, E(io.ErrUnexpectedEOF).withPos(`["",`, ""), ""}, diff --git a/jsontext/encode_test.go b/jsontext/encode_test.go index 372a1f3..b6d4a33 100644 --- a/jsontext/encode_test.go +++ b/jsontext/encode_test.go @@ -172,7 +172,7 @@ var encoderErrorTestdata = []struct { }, { name: jsontest.Name("InvalidNull"), calls: []encoderMethodCall{ - {Value(`nulL`), newInvalidCharacterError("L", "within literal null (expecting 'l')").withPos(`nul`, ""), ""}, + {Value(`nulL`), newInvalidCharacterError("L", "in literal null (expecting 'l')").withPos(`nul`, ""), ""}, }, }, { name: jsontest.Name("TruncatedFalse"), @@ -182,7 +182,7 @@ var encoderErrorTestdata = []struct { }, { name: jsontest.Name("InvalidFalse"), calls: []encoderMethodCall{ - {Value(`falsE`), newInvalidCharacterError("E", "within literal false (expecting 'e')").withPos(`fals`, ""), ""}, + {Value(`falsE`), newInvalidCharacterError("E", "in literal false (expecting 'e')").withPos(`fals`, ""), ""}, }, }, { name: jsontest.Name("TruncatedTrue"), @@ -192,7 +192,7 @@ var encoderErrorTestdata = []struct { }, { name: jsontest.Name("InvalidTrue"), calls: []encoderMethodCall{ - {Value(`truE`), newInvalidCharacterError("E", "within literal true (expecting 'e')").withPos(`tru`, ""), ""}, + {Value(`truE`), newInvalidCharacterError("E", "in literal true (expecting 'e')").withPos(`tru`, ""), ""}, }, }, { name: jsontest.Name("TruncatedString"), @@ -202,7 +202,7 @@ var encoderErrorTestdata = []struct { }, { name: jsontest.Name("InvalidString"), calls: []encoderMethodCall{ - {Value(`"ok` + "\x00"), newInvalidCharacterError("\x00", `within string (expecting non-control character)`).withPos(`"ok`, ""), ""}, + {Value(`"ok` + "\x00"), newInvalidCharacterError("\x00", `in string (expecting non-control character)`).withPos(`"ok`, ""), ""}, }, }, { name: jsontest.Name("ValidString/AllowInvalidUTF8/Token"), @@ -239,7 +239,7 @@ var encoderErrorTestdata = []struct { }, { name: jsontest.Name("InvalidNumber"), calls: []encoderMethodCall{ - {Value(`0.e`), newInvalidCharacterError("e", "within number (expecting digit)").withPos(`0.`, ""), ""}, + {Value(`0.e`), newInvalidCharacterError("e", "in number (expecting digit)").withPos(`0.`, ""), ""}, }, }, { name: jsontest.Name("TruncatedObject/AfterStart"), diff --git a/jsontext/errors.go b/jsontext/errors.go index a466b1a..eb5f0e2 100644 --- a/jsontext/errors.go +++ b/jsontext/errors.go @@ -67,6 +67,20 @@ func wrapSyntacticError(state interface { ptr = serr.appendPointer(ptr) err = serr.error } + if d, ok := state.(*decoderState); ok && err == errMismatchDelim { + where := "at start of value" + if len(d.Tokens.Stack) > 0 && d.Tokens.Last.Length() > 0 { + switch { + case d.Tokens.Last.isArray(): + where = "after array element (expecting ',' or ']')" + ptr = []byte(Pointer(ptr).Parent()) // problem is with parent array + case d.Tokens.Last.isObject(): + where = "after object value (expecting ',' or '}')" + ptr = []byte(Pointer(ptr).Parent()) // problem is with parent object + } + } + err = jsonwire.NewInvalidCharacterError(d.buf[pos:], where) + } return &SyntacticError{ByteOffset: offset, JSONPointer: Pointer(ptr), Err: err} } diff --git a/jsontext/state.go b/jsontext/state.go index 6b200c1..9278b5c 100644 --- a/jsontext/state.go +++ b/jsontext/state.go @@ -38,9 +38,7 @@ var ( // This error is directly wrapped within a [SyntacticError] when produced. ErrNonStringName = errors.New("object member name must be a string") - errMissingColon = errors.New("missing character ':' after object name") errMissingValue = errors.New("missing value after object name") - errMissingComma = errors.New("missing character ',' after object or array value") errMismatchDelim = errors.New("mismatching structural token for object or array") errMaxDepth = errors.New("exceeded max depth") @@ -393,21 +391,6 @@ func (m stateMachine) needDelim(next Kind) (delim byte) { } } -// checkDelim reports whether the specified delimiter should be there given -// the kind of the next token that appears immediately afterwards. -func (m stateMachine) checkDelim(delim byte, next Kind) error { - switch m.needDelim(next) { - case delim: - return nil - case ':': - return errMissingColon - case ',': - return errMissingComma - default: - return jsonwire.NewInvalidCharacterError([]byte{delim}, "before next token") - } -} - // InvalidateDisabledNamespaces marks all disabled namespaces as invalid. // // For efficiency, Marshal and Unmarshal may disable namespaces since there are diff --git a/jsontext/token.go b/jsontext/token.go index 166e485..adb91e5 100644 --- a/jsontext/token.go +++ b/jsontext/token.go @@ -22,7 +22,7 @@ const ( maxUint64 = math.MaxUint64 minUint64 = 0 // for consistency and readability purposes - invalidTokenPanic = "invalid json.Token; it has been voided by a subsequent json.Decoder call" + invalidTokenPanic = "invalid jsontext.Token; it has been voided by a subsequent json.Decoder call" ) var errInvalidToken = errors.New("invalid jsontext.Token") @@ -271,7 +271,7 @@ func (t Token) string() (string, []byte) { return strconv.FormatUint(uint64(t.num), 10), nil } } - return "", nil + return "", nil } // appendNumber appends a JSON number to dst and returns it. @@ -515,7 +515,7 @@ func (k Kind) String() string { case ']': return "]" default: - return "" + return "" } } diff --git a/jsontext/token_test.go b/jsontext/token_test.go index 457aefb..2180b6a 100644 --- a/jsontext/token_test.go +++ b/jsontext/token_test.go @@ -43,7 +43,7 @@ func TestTokenAccessors(t *testing.T) { in Token want token }{ - {Token{}, token{String: ""}}, + {Token{}, token{String: ""}}, {Null, token{String: "null", Kind: 'n'}}, {False, token{Bool: false, String: "false", Kind: 'f'}}, {True, token{Bool: true, String: "true", Kind: 't'}}, diff --git a/jsontext/value.go b/jsontext/value.go index 912088c..20da98f 100644 --- a/jsontext/value.go +++ b/jsontext/value.go @@ -123,7 +123,7 @@ func (v Value) MarshalJSON() ([]byte, error) { func (v *Value) UnmarshalJSON(b []byte) error { // NOTE: This matches the behavior of v1 json.RawMessage.UnmarshalJSON. if v == nil { - return errors.New("json.Value: UnmarshalJSON on nil pointer") + return errors.New("jsontext.Value: UnmarshalJSON on nil pointer") } *v = append((*v)[:0], b...) return nil @@ -153,10 +153,10 @@ func (v *Value) reformat(canonical, multiline bool, prefix, indent string) error eo.Flags.Set(jsonflags.Multiline | 0) // per RFC 8785, section 3.2.1 } else { if s := strings.TrimLeft(prefix, " \t"); len(s) > 0 { - panic("json: invalid character " + jsonwire.QuoteRune(s) + " in indent prefix") + panic("jsontext: invalid character " + jsonwire.QuoteRune(s) + " in indent prefix") } if s := strings.TrimLeft(indent, " \t"); len(s) > 0 { - panic("json: invalid character " + jsonwire.QuoteRune(s) + " in indent") + panic("jsontext: invalid character " + jsonwire.QuoteRune(s) + " in indent") } eo.Flags.Set(jsonflags.AllowInvalidUTF8 | 1) eo.Flags.Set(jsonflags.AllowDuplicateNames | 1) diff --git a/migrate.sh b/migrate.sh index c5bd9bb..a16c6e0 100755 --- a/migrate.sh +++ b/migrate.sh @@ -31,11 +31,6 @@ sed -i 's/json\.struct/v2.struct/g' $GOROOT/src/encoding/json/v2/errors_test.go sed -i 's|"encoding/json"|"encoding/json", "encoding/json/v2"|g' $GOROOT/src/cmd/vendor/golang.org/x/tools/go/analysis/passes/structtag/structtag.go # Adjust tests that hardcode formatted error strings. -sed -i 's/looking for beginning of value/at start of value/g' $GOROOT/src/cmd/go/testdata/script/mod_list_update_nolatest.txt -sed -i 's/looking for beginning of value/at start of value/g' $GOROOT/src/cmd/go/testdata/script/mod_proxy_invalid.txt -sed -i 's/looking for beginning of value/at start of value/g' $GOROOT/src/cmd/go/testdata/script/test_fuzz_io_error.txt -sed -i 's/: invalid character/: jsontext: invalid character/g' $GOROOT/src/html/template/escape_test.go -sed -i 's/looking for beginning of object key string/at start of string (expecting \'\"\') after offset 2/g' $GOROOT/src/html/template/escape_test.go sed -i 's/}`, "Time.UnmarshalJSON: input is not a JSON string/}`, "json: cannot unmarshal JSON object into Go type time.Time/g' $GOROOT/src/time/time_test.go sed -i 's/]`, "Time.UnmarshalJSON: input is not a JSON string/]`, "json: cannot unmarshal JSON array into Go type time.Time/g' $GOROOT/src/time/time_test.go diff --git a/v1/decode_test.go b/v1/decode_test.go index 5f1e3db..1bf9111 100644 --- a/v1/decode_test.go +++ b/v1/decode_test.go @@ -471,20 +471,20 @@ var unmarshalTests = []struct { {CaseName: Name(""), in: `{"alphabet": "xyz"}`, ptr: new(U), err: fmt.Errorf("json: unknown field \"alphabet\""), disallowUnknownFields: true}, // syntax errors - {CaseName: Name(""), in: `{"X": "foo", "Y"}`, err: &SyntaxError{"invalid character '}' after object name (expecting ':')", len64(`{"X": "foo", "Y"`)}}, - {CaseName: Name(""), in: `[1, 2, 3+]`, err: &SyntaxError{"invalid character '+' after array value (expecting ',' or ']')", len64(`[1, 2, 3`)}}, - {CaseName: Name(""), in: `{"X":12x}`, err: &SyntaxError{"invalid character 'x' after object value (expecting ',' or '}')", len64(`{"X":12`)}, useNumber: true}, + {CaseName: Name(""), in: `{"X": "foo", "Y"}`, err: &SyntaxError{"invalid character '}' after object key", len64(`{"X": "foo", "Y"`)}}, + {CaseName: Name(""), in: `[1, 2, 3+]`, err: &SyntaxError{"invalid character '+' after array element", len64(`[1, 2, 3`)}}, + {CaseName: Name(""), in: `{"X":12x}`, err: &SyntaxError{"invalid character 'x' after object key:value pair", len64(`{"X":12`)}, useNumber: true}, {CaseName: Name(""), in: `[2, 3`, err: &SyntaxError{msg: "unexpected end of JSON input", Offset: len64(`[2, 3`)}}, - {CaseName: Name(""), in: `{"F3": -}`, ptr: new(V), err: &SyntaxError{msg: "invalid character '}' within number (expecting digit)", Offset: len64(`{"F3": -`)}}, + {CaseName: Name(""), in: `{"F3": -}`, ptr: new(V), err: &SyntaxError{msg: "invalid character '}' in numeric literal", Offset: len64(`{"F3": -`)}}, // raw value errors - {CaseName: Name(""), in: "\x01 42", err: &SyntaxError{"invalid character '\\x01' at start of value", len64(``)}}, + {CaseName: Name(""), in: "\x01 42", err: &SyntaxError{"invalid character '\\x01' looking for beginning of value", len64(``)}}, {CaseName: Name(""), in: " 42 \x01", err: &SyntaxError{"invalid character '\\x01' after top-level value", len64(` 42 `)}}, - {CaseName: Name(""), in: "\x01 true", err: &SyntaxError{"invalid character '\\x01' at start of value", len64(``)}}, + {CaseName: Name(""), in: "\x01 true", err: &SyntaxError{"invalid character '\\x01' looking for beginning of value", len64(``)}}, {CaseName: Name(""), in: " false \x01", err: &SyntaxError{"invalid character '\\x01' after top-level value", len64(` false `)}}, - {CaseName: Name(""), in: "\x01 1.2", err: &SyntaxError{"invalid character '\\x01' at start of value", len64(``)}}, + {CaseName: Name(""), in: "\x01 1.2", err: &SyntaxError{"invalid character '\\x01' looking for beginning of value", len64(``)}}, {CaseName: Name(""), in: " 3.4 \x01", err: &SyntaxError{"invalid character '\\x01' after top-level value", len64(` 3.4 `)}}, - {CaseName: Name(""), in: "\x01 \"string\"", err: &SyntaxError{"invalid character '\\x01' at start of value", len64(``)}}, + {CaseName: Name(""), in: "\x01 \"string\"", err: &SyntaxError{"invalid character '\\x01' looking for beginning of value", len64(``)}}, {CaseName: Name(""), in: " \"string\" \x01", err: &SyntaxError{"invalid character '\\x01' after top-level value", len64(` "string" `)}}, // array tests @@ -577,42 +577,42 @@ var unmarshalTests = []struct { in: `{"abc":"abc"}`, ptr: new(map[int]string), out: map[int]string{}, - err: &UnmarshalTypeError{Value: `string "abc"`, Type: reflect.TypeFor[int](), Field: "abc", Offset: len64(`{`), Err: strconv.ErrSyntax}, + err: &UnmarshalTypeError{Value: "number abc", Type: reflect.TypeFor[int](), Field: "abc", Offset: len64(`{`)}, }, { CaseName: Name(""), in: `{"256":"abc"}`, ptr: new(map[uint8]string), out: map[uint8]string{}, - err: &UnmarshalTypeError{Value: `string "256"`, Type: reflect.TypeFor[uint8](), Field: "256", Offset: len64(`{`), Err: strconv.ErrRange}, + err: &UnmarshalTypeError{Value: "number 256", Type: reflect.TypeFor[uint8](), Field: "256", Offset: len64(`{`)}, }, { CaseName: Name(""), in: `{"128":"abc"}`, ptr: new(map[int8]string), out: map[int8]string{}, - err: &UnmarshalTypeError{Value: `string "128"`, Type: reflect.TypeFor[int8](), Field: "128", Offset: len64(`{`), Err: strconv.ErrRange}, + err: &UnmarshalTypeError{Value: "number 128", Type: reflect.TypeFor[int8](), Field: "128", Offset: len64(`{`)}, }, { CaseName: Name(""), in: `{"-1":"abc"}`, ptr: new(map[uint8]string), out: map[uint8]string{}, - err: &UnmarshalTypeError{Value: `string "-1"`, Type: reflect.TypeFor[uint8](), Field: "-1", Offset: len64(`{`), Err: strconv.ErrSyntax}, + err: &UnmarshalTypeError{Value: "number -1", Type: reflect.TypeFor[uint8](), Field: "-1", Offset: len64(`{`)}, }, { CaseName: Name(""), in: `{"F":{"a":2,"3":4}}`, ptr: new(map[string]map[int]int), out: map[string]map[int]int{"F": {3: 4}}, - err: &UnmarshalTypeError{Value: `string "a"`, Type: reflect.TypeFor[int](), Field: "F.a", Offset: len64(`{"F":{`), Err: strconv.ErrSyntax}, + err: &UnmarshalTypeError{Value: "number a", Type: reflect.TypeFor[int](), Field: "F.a", Offset: len64(`{"F":{`)}, }, { CaseName: Name(""), in: `{"F":{"a":2,"3":4}}`, ptr: new(map[string]map[uint]int), out: map[string]map[uint]int{"F": {3: 4}}, - err: &UnmarshalTypeError{Value: `string "a"`, Type: reflect.TypeFor[uint](), Field: "F.a", Offset: len64(`{"F":{`), Err: strconv.ErrSyntax}, + err: &UnmarshalTypeError{Value: "number a", Type: reflect.TypeFor[uint](), Field: "F.a", Offset: len64(`{"F":{`)}, }, // Map keys can be encoding.TextUnmarshalers. @@ -1102,7 +1102,7 @@ var unmarshalTests = []struct { in: `invalid`, ptr: new(Number), err: &SyntaxError{ - msg: "invalid character 'i' at start of value", + msg: "invalid character 'i' looking for beginning of value", Offset: len64(``), }, }, @@ -1184,14 +1184,14 @@ var unmarshalTests = []struct { CaseName: Name(""), in: `[1,2,true,4,5}`, ptr: new([]int), - err: &SyntaxError{msg: "invalid character '}' after array value (expecting ',' or ']')", Offset: len64(`[1,2,true,4,5`)}, + err: &SyntaxError{msg: "invalid character '}' after array element", Offset: len64(`[1,2,true,4,5`)}, }, { CaseName: Name(""), in: `[1,2,true,4,5]`, ptr: new([]int), out: []int{1, 2, 0, 4, 5}, - err: &UnmarshalTypeError{Value: "true", Type: reflect.TypeFor[int](), Field: "2", Offset: len64(`[1,2,`)}, + err: &UnmarshalTypeError{Value: "bool", Type: reflect.TypeFor[int](), Field: "2", Offset: len64(`[1,2,`)}, }, } @@ -1518,12 +1518,12 @@ func TestErrorMessageFromMisusedString(t *testing.T) { CaseName in, err string }{ - {Name(""), `{"result":"x"}`, `json: cannot unmarshal JSON string into WrongString.result of Go type string: jsontext: invalid character 'x' at start of string (expecting '"')`}, - {Name(""), `{"result":"foo"}`, `json: cannot unmarshal JSON string into WrongString.result of Go type string: jsontext: invalid character 'f' at start of string (expecting '"')`}, - {Name(""), `{"result":"123"}`, `json: cannot unmarshal JSON string into WrongString.result of Go type string: jsontext: invalid character '1' at start of string (expecting '"')`}, + {Name(""), `{"result":"x"}`, `json: cannot unmarshal JSON string into WrongString.result of Go type string: invalid character 'x' looking for beginning of object key string`}, + {Name(""), `{"result":"foo"}`, `json: cannot unmarshal JSON string into WrongString.result of Go type string: invalid character 'f' looking for beginning of object key string`}, + {Name(""), `{"result":"123"}`, `json: cannot unmarshal JSON string into WrongString.result of Go type string: invalid character '1' looking for beginning of object key string`}, {Name(""), `{"result":123}`, `json: cannot unmarshal JSON number into WrongString.result of Go type string`}, - {Name(""), `{"result":"\""}`, `json: cannot unmarshal JSON string into WrongString.result of Go type string: jsontext: unexpected EOF`}, - {Name(""), `{"result":"\"foo"}`, `json: cannot unmarshal JSON string into WrongString.result of Go type string: jsontext: unexpected EOF`}, + {Name(""), `{"result":"\""}`, `json: cannot unmarshal JSON string into WrongString.result of Go type string: unexpected end of JSON input`}, + {Name(""), `{"result":"\"foo"}`, `json: cannot unmarshal JSON string into WrongString.result of Go type string: unexpected end of JSON input`}, } for _, tt := range tests { t.Run(tt.Name, func(t *testing.T) { @@ -2606,23 +2606,23 @@ func TestUnmarshalErrorAfterMultipleJSON(t *testing.T) { }{{ CaseName: Name(""), in: `1 false null :`, - err: &SyntaxError{"invalid character ':' before next token", len64(`1 false null `)}, + err: &SyntaxError{"invalid character ':' looking for beginning of value", len64(`1 false null `)}, }, { CaseName: Name(""), in: `1 [] [,]`, - err: &SyntaxError{"invalid character ',' at start of value", len64(`1 [] [`)}, + err: &SyntaxError{"invalid character ',' looking for beginning of value", len64(`1 [] [`)}, }, { CaseName: Name(""), in: `1 [] [true:]`, - err: &SyntaxError{"invalid character ':' after array value (expecting ',' or ']')", len64(`1 [] [true`)}, + err: &SyntaxError{"invalid character ':' after array element", len64(`1 [] [true`)}, }, { CaseName: Name(""), in: `1 {} {"x"=}`, - err: &SyntaxError{"invalid character '=' after object name (expecting ':')", len64(`1 {} {"x"`)}, + err: &SyntaxError{"invalid character '=' after object key", len64(`1 {} {"x"`)}, }, { CaseName: Name(""), in: `falsetruenul#`, - err: &SyntaxError{"invalid character '#' within literal null (expecting 'l')", len64(`falsetruenul`)}, + err: &SyntaxError{"invalid character '#' in literal null (expecting 'l')", len64(`falsetruenul`)}, }} for _, tt := range tests { t.Run(tt.Name, func(t *testing.T) { diff --git a/v1/inject.go b/v1/inject.go index 1a92455..045c569 100644 --- a/v1/inject.go +++ b/v1/inject.go @@ -7,10 +7,12 @@ package json import ( "fmt" "reflect" + "strconv" "strings" jsonv2 "github.com/go-json-experiment/json" "github.com/go-json-experiment/json/internal" + "github.com/go-json-experiment/json/jsontext" ) // Inject functionality into v2 to properly handle v1 types. @@ -52,6 +54,10 @@ func transformMarshalError(root any, err error) error { } else if ok { return (*UnsupportedValueError)(nil) } + if err, _ := err.(*MarshalerError); err != nil { + err.Err = transformSyntacticError(err.Err) + return err + } return transformSyntacticError(err) } @@ -88,14 +94,36 @@ func transformUnmarshalError(root any, err error) error { // See https://go.dev/issue/43126 var value string switch err.JSONKind { - case 'n', 'f', 't', '"', '0': + case 'n', '"', '0': value = err.JSONKind.String() + case 'f', 't': + value = "bool" case '[', ']': value = "array" case '{', '}': value = "object" } if len(err.JSONValue) > 0 { + isStrconvError := err.Err == strconv.ErrRange || err.Err == strconv.ErrSyntax + isNumericKind := func(t reflect.Type) bool { + if t == nil { + return false + } + switch t.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr, + reflect.Float32, reflect.Float64: + return true + } + return false + } + if isStrconvError && isNumericKind(err.GoType) { + value = "number" + if err.JSONKind == '"' { + err.JSONValue, _ = jsontext.AppendUnquote(nil, err.JSONValue) + } + err.Err = nil + } value += " " + string(err.JSONValue) } var rootName string @@ -114,7 +142,7 @@ func transformUnmarshalError(root any, err error) error { Offset: err.ByteOffset, Struct: rootName, Field: fieldPath, - Err: err.Err, + Err: transformSyntacticError(err.Err), } } else if ok { return (*UnmarshalTypeError)(nil) diff --git a/v1/scanner.go b/v1/scanner.go index e18fd09..025fd40 100644 --- a/v1/scanner.go +++ b/v1/scanner.go @@ -7,6 +7,7 @@ package json import ( "errors" "io" + "strings" "github.com/go-json-experiment/json/internal" "github.com/go-json-experiment/json/internal/jsonflags" @@ -52,7 +53,11 @@ func transformSyntacticError(err error) error { if serr.Err == io.ErrUnexpectedEOF { serr.Err = errUnexpectedEnd } - return &SyntaxError{Offset: serr.ByteOffset, msg: serr.Err.Error()} + msg := serr.Err.Error() + if i := strings.Index(msg, " (expecting"); i >= 0 && !strings.Contains(msg, " in literal") { + msg = msg[:i] + } + return &SyntaxError{Offset: serr.ByteOffset, msg: syntaxErrorReplacer.Replace(msg)} case ok: return (*SyntaxError)(nil) case export.IsIOError(err): @@ -61,3 +66,15 @@ func transformSyntacticError(err error) error { return err } } + +// syntaxErrorReplacer replaces certain string literals in the v2 error +// to better match the historical string rendering of syntax errors. +// In particular, v2 uses the terminology "object name" to match RFC 8259, +// while v1 uses "object key", which is not a term found in JSON literature. +var syntaxErrorReplacer = strings.NewReplacer( + "object name", "object key", + "at start of value", "looking for beginning of value", + "at start of string", "looking for beginning of object key string", + "after object value", "after object key:value pair", + "in number", "in numeric literal", +) diff --git a/v1/scanner_test.go b/v1/scanner_test.go index 8b31b23..f0b1197 100644 --- a/v1/scanner_test.go +++ b/v1/scanner_test.go @@ -190,8 +190,8 @@ func TestIndentErrors(t *testing.T) { in string err error }{ - {Name(""), `{"X": "foo", "Y"}`, &SyntaxError{"invalid character '}' after object name (expecting ':')", len64(`{"X": "foo", "Y"`)}}, - {Name(""), `{"X": "foo" "Y": "bar"}`, &SyntaxError{"invalid character '\"' after object value (expecting ',' or '}')", len64(`{"X": "foo" `)}}, + {Name(""), `{"X": "foo", "Y"}`, &SyntaxError{"invalid character '}' after object key", len64(`{"X": "foo", "Y"`)}}, + {Name(""), `{"X": "foo" "Y": "bar"}`, &SyntaxError{"invalid character '\"' after object key:value pair", len64(`{"X": "foo" `)}}, } for _, tt := range tests { t.Run(tt.Name, func(t *testing.T) { diff --git a/v1/stream.go b/v1/stream.go index 308113b..03c12e1 100644 --- a/v1/stream.go +++ b/v1/stream.go @@ -66,6 +66,11 @@ func (dec *Decoder) Decode(v any) error { b, err := dec.dec.ReadValue() if err != nil { dec.err = transformSyntacticError(err) + if dec.err == errUnexpectedEnd { + // NOTE: Decode has always been inconsistent with Unmarshal + // with regard to the exact error value for truncated input. + dec.err = io.ErrUnexpectedEOF + } return dec.err } return jsonv2.Unmarshal(b, v, dec.opts) diff --git a/v1/stream_test.go b/v1/stream_test.go index 38bc0dc..1193db3 100644 --- a/v1/stream_test.go +++ b/v1/stream_test.go @@ -424,18 +424,18 @@ func TestDecodeInStream(t *testing.T) { {CaseName: Name(""), json: ` [{"a": 1} {"a": 2}] `, expTokens: []any{ Delim('['), decodeThis{map[string]any{"a": float64(1)}}, - decodeThis{&SyntaxError{"missing character ',' after object or array value", len64(` [{"a": 1} `)}}, + decodeThis{&SyntaxError{"invalid character '{' after array element", len64(` [{"a": 1} `)}}, }}, {CaseName: Name(""), json: `{ "` + strings.Repeat("a", 513) + `" 1 }`, expTokens: []any{ Delim('{'), strings.Repeat("a", 513), - decodeThis{&SyntaxError{"missing character ':' after object name", len64(`{ "` + strings.Repeat("a", 513) + `" `)}}, + decodeThis{&SyntaxError{"invalid character '1' after object key", len64(`{ "` + strings.Repeat("a", 513) + `" `)}}, }}, {CaseName: Name(""), json: `{ "\a" }`, expTokens: []any{ Delim('{'), - &SyntaxError{"invalid escape sequence `\\a` within string", len64(`{ "`)}, + &SyntaxError{"invalid escape sequence `\\a` in string", len64(`{ "`)}, }}, {CaseName: Name(""), json: ` \a`, expTokens: []any{ - &SyntaxError{"invalid character '\\\\' at start of token", len64(` `)}, + &SyntaxError{"invalid character '\\\\' looking for beginning of value", len64(` `)}, }}, } for _, tt := range tests {