journal/gojq-extended/error.go
2024-11-10 08:13:25 +00:00

226 lines
4.5 KiB
Go

package main
import (
"encoding/json"
"errors"
"fmt"
"io"
"strconv"
"strings"
"unicode/utf8"
"github.com/mattn/go-runewidth"
"github.com/itchyny/gojq"
)
type emptyError struct {
err error
}
func (*emptyError) Error() string {
return ""
}
func (*emptyError) isEmptyError() {}
func (err *emptyError) ExitCode() int {
if err, ok := err.err.(interface{ ExitCode() int }); ok {
return err.ExitCode()
}
return exitCodeDefaultErr
}
type exitCodeError struct {
code int
}
func (err *exitCodeError) Error() string {
return "exit code: " + strconv.Itoa(err.code)
}
func (*exitCodeError) isEmptyError() {}
func (err *exitCodeError) ExitCode() int {
return err.code
}
type flagParseError struct {
err error
}
func (err *flagParseError) Error() string {
return err.err.Error()
}
func (*flagParseError) ExitCode() int {
return exitCodeFlagParseErr
}
type compileError struct {
err error
}
func (err *compileError) Error() string {
return "compile error: " + err.err.Error()
}
func (*compileError) ExitCode() int {
return exitCodeCompileErr
}
type queryParseError struct {
fname, contents string
err error
}
func (err *queryParseError) Error() string {
var offset int
var e *gojq.ParseError
if errors.As(err.err, &e) {
offset = e.Offset - len(e.Token) + 1
}
linestr, line, column := getLineByOffset(err.contents, offset)
if err.fname != "<arg>" || containsNewline(err.contents) {
return fmt.Sprintf("invalid query: %s:%d\n%s %s",
err.fname, line, formatLineInfo(linestr, line, column), err.err)
}
return fmt.Sprintf("invalid query: %s\n %s\n %*c %s",
err.contents, linestr, column+1, '^', err.err)
}
func (*queryParseError) ExitCode() int {
return exitCodeCompileErr
}
type jsonParseError struct {
fname, contents string
line int
err error
}
func (err *jsonParseError) Error() string {
var offset int
if err.err == io.ErrUnexpectedEOF {
offset = len(err.contents) + 1
} else if e, ok := err.err.(*json.SyntaxError); ok {
offset = int(e.Offset)
}
linestr, line, column := getLineByOffset(err.contents, offset)
if line += err.line; line > 1 {
return fmt.Sprintf("invalid json: %s:%d\n%s %s",
err.fname, line, formatLineInfo(linestr, line, column), err.err)
}
return fmt.Sprintf("invalid json: %s\n %s\n %*c %s",
err.fname, linestr, column+1, '^', err.err)
}
func getLineByOffset(str string, offset int) (linestr string, line, column int) {
ss := &stringScanner{str, 0}
for {
str, start, ok := ss.next()
if !ok {
offset -= start
break
}
line++
linestr = str
if ss.offset >= offset {
offset -= start
break
}
}
offset = min(max(offset-1, 0), len(linestr))
if offset > 48 {
skip := len(trimLastInvalidRune(linestr[:offset-48]))
linestr = linestr[skip:]
offset -= skip
}
linestr = trimLastInvalidRune(linestr[:min(64, len(linestr))])
if offset < len(linestr) {
offset = len(trimLastInvalidRune(linestr[:offset]))
} else {
offset = len(linestr)
}
column = runewidth.StringWidth(linestr[:offset])
return
}
func getLineByLine(str string, line int) (linestr string) {
ss := &stringScanner{str, 0}
for {
str, _, ok := ss.next()
if !ok {
break
}
if line--; line == 0 {
linestr = str
break
}
}
if len(linestr) > 64 {
linestr = trimLastInvalidRune(linestr[:64])
}
return
}
func trimLastInvalidRune(s string) string {
for i := len(s) - 1; i >= 0 && i > len(s)-utf8.UTFMax; i-- {
if b := s[i]; b < utf8.RuneSelf {
return s[:i+1]
} else if utf8.RuneStart(b) {
if r, _ := utf8.DecodeRuneInString(s[i:]); r == utf8.RuneError {
return s[:i]
}
break
}
}
return s
}
func formatLineInfo(linestr string, line, column int) string {
l := strconv.Itoa(line)
return fmt.Sprintf(" %s | %s\n %*c", l, linestr, column+len(l)+4, '^')
}
type stringScanner struct {
str string
offset int
}
func (ss *stringScanner) next() (line string, start int, ok bool) {
if ss.offset == len(ss.str) {
return
}
start, ok = ss.offset, true
line = ss.str[start:]
i := indexNewline(line)
if i < 0 {
ss.offset = len(ss.str)
return
}
line = line[:i]
if strings.HasPrefix(ss.str[start+i:], "\r\n") {
i++
}
ss.offset += i + 1
return
}
// Faster than strings.ContainsAny(str, "\r\n").
func containsNewline(str string) bool {
return strings.IndexByte(str, '\n') >= 0 ||
strings.IndexByte(str, '\r') >= 0
}
// Faster than strings.IndexAny(str, "\r\n").
func indexNewline(str string) (i int) {
if i = strings.IndexByte(str, '\n'); i >= 0 {
str = str[:i]
}
if j := strings.IndexByte(str, '\r'); j >= 0 {
i = j
}
return
}