Display better error messages on json.SyntaxError

Better display an error message on an encounter of a json.SyntaxError.

Rolls back the file position, to read the entire file, then steps
through the file reading a single byte at a time, populating lines until
encountering the syntax error. Then relays the offending line as well as
the previous line in the file to the user, also placing a `^` that
points the the offending column of the decoder error.

```
➤  packer validate template.json
Failed to parse template: Error parsing JSON: invalid character '"' after object key:value pair
At line 9, column 8 (offset 121):
    8:       "name": "vbox"
    9:       "
```
This commit is contained in:
Jake Champlin 2016-02-10 14:52:26 -05:00
parent 314aad379a
commit aca4aed47c
2 changed files with 50 additions and 23 deletions

View File

@ -10,6 +10,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"sort" "sort"
"strings"
"github.com/hashicorp/go-multierror" "github.com/hashicorp/go-multierror"
"github.com/mitchellh/mapstructure" "github.com/mitchellh/mapstructure"
@ -311,24 +312,6 @@ func Parse(r io.Reader) (*Template, error) {
return rawTpl.Template() return rawTpl.Template()
} }
// Find line number and position based on the offset
func findLinePos(f *os.File, offset int64) (int64, int64, string) {
scanner := bufio.NewScanner(f)
count := int64(0)
for scanner.Scan() {
count += 1
scanLength := len(scanner.Text()) + 1
if offset < int64(scanLength) {
return count, offset, scanner.Text()
}
offset = offset - int64(scanLength)
}
if err := scanner.Err(); err != nil {
return 0, 0, err.Error()
}
return 0, 0, ""
}
// ParseFile is the same as Parse but is a helper to automatically open // ParseFile is the same as Parse but is a helper to automatically open
// a file for parsing. // a file for parsing.
func ParseFile(path string) (*Template, error) { func ParseFile(path string) (*Template, error) {
@ -359,8 +342,9 @@ func ParseFile(path string) (*Template, error) {
} }
// Rewind the file and get a better error // Rewind the file and get a better error
f.Seek(0, os.SEEK_SET) f.Seek(0, os.SEEK_SET)
line, pos, errorLine := findLinePos(f, syntaxErr.Offset) // Grab the error location, and return a string to point to offending syntax error
err = fmt.Errorf("Error in line %d, char %d: %s\n%s", line, pos, syntaxErr, errorLine) line, col, highlight := highlightPosition(f, syntaxErr.Offset)
err = fmt.Errorf("Error parsing JSON: %s\nAt line %d, column %d (offset %d):\n%s", err, line, col, syntaxErr.Offset, highlight)
return nil, err return nil, err
} }
@ -374,3 +358,46 @@ func ParseFile(path string) (*Template, error) {
tpl.Path = path tpl.Path = path
return tpl, nil return tpl, nil
} }
// Takes a file and the location in bytes of a parse error
// from json.SyntaxError.Offset and returns the line, column,
// and pretty-printed context around the error with an arrow indicating the exact
// position of the syntax error.
func highlightPosition(f *os.File, pos int64) (line, col int, highlight string) {
// Modified version of the function in Camlistore by Brad Fitzpatrick
// https://github.com/camlistore/camlistore/blob/4b5403dd5310cf6e1ae8feb8533fd59262701ebc/vendor/go4.org/errorutil/highlight.go
line = 1
// New io.Reader for file
br := bufio.NewReader(f)
// Initialize lines
lastLine := ""
thisLine := new(bytes.Buffer)
// Loop through template to find line, column
for n := int64(0); n < pos; n++ {
// read byte from io.Reader
b, err := br.ReadByte()
if err != nil {
break
}
// If end of line, save line as previous line in case next line is offender
if b == '\n' {
lastLine = thisLine.String()
thisLine.Reset()
line++
col = 1
} else {
// Write current line, until line is safe, or error point is encountered
col++
thisLine.WriteByte(b)
}
}
// Populate highlight string to place a '^' char at offending column
if line > 1 {
highlight += fmt.Sprintf("%5d: %s\n", line-1, lastLine)
}
highlight += fmt.Sprintf("%5d: %s\n", line, thisLine.String())
highlight += fmt.Sprintf("%s^\n", strings.Repeat(" ", col+5))
return
}

View File

@ -356,9 +356,9 @@ func TestParse_bad(t *testing.T) {
File string File string
Expected string Expected string
}{ }{
{"error-beginning.json", "line 1, char 1"}, {"error-beginning.json", "line 1, column 1 (offset 1)"},
{"error-middle.json", "line 5, char 5"}, {"error-middle.json", "line 5, column 6 (offset 50)"},
{"error-end.json", "line 1, char 30"}, {"error-end.json", "line 1, column 30 (offset 30)"},
} }
for _, tc := range cases { for _, tc := range cases {
_, err := ParseFile(fixtureDir(tc.File)) _, err := ParseFile(fixtureDir(tc.File))