Repository: goccy/go-yaml
Pull request #1 (2019-10-23 04:38:42)

Improve error printing for Syntax Error
Support Beautiful syntax error notification

![スクリーンショット 2019-10-23 13 37 01](https://user-images.githubusercontent.com/209884/67358124-587f0980-f59a-11e9-96fc-7205aab77695.png)




Patch:
diff --git a/errors/error.go b/errors/error.go
new file mode 100644
index 0000000..0162644
--- /dev/null
+++ b/errors/error.go
@@ -0,0 +1,158 @@
+package errors
+
+import (
+	"fmt"
+
+	"github.com/goccy/go-yaml/printer"
+	"github.com/goccy/go-yaml/token"
+	"golang.org/x/xerrors"
+)
+
+var (
+	// ColoredErr error with syntax highlight
+	ColoredErr = true
+	// WithSourceCode error with source code
+	WithSourceCode = true
+)
+
+// Wrapf wrap error for stack trace
+func Wrapf(err error, msg string, args ...interface{}) error {
+	return &wrapError{
+		baseError: &baseError{},
+		err:       xerrors.Errorf(msg, args...),
+		nextErr:   err,
+		frame:     xerrors.Caller(1),
+	}
+}
+
+// ErrSyntax create syntax error instance with message and token
+func ErrSyntax(msg string, tk *token.Token) *syntaxError {
+	return &syntaxError{
+		baseError: &baseError{},
+		Msg:       msg,
+		Token:     tk,
+		frame:     xerrors.Caller(1),
+	}
+}
+
+type baseError struct {
+	state fmt.State
+	verb  rune
+}
+
+func (e *baseError) Error() string {
+	return ""
+}
+
+func (e *baseError) chainStateAndVerb(err error) {
+	wrapErr, ok := err.(*wrapError)
+	if ok {
+		wrapErr.state = e.state
+		wrapErr.verb = e.verb
+	}
+	syntaxErr, ok := err.(*syntaxError)
+	if ok {
+		syntaxErr.state = e.state
+		syntaxErr.verb = e.verb
+	}
+}
+
+type wrapError struct {
+	*baseError
+	err     error
+	nextErr error
+	frame   xerrors.Frame
+}
+
+func (e *wrapError) FormatError(p xerrors.Printer) error {
+	if e.verb == 'v' && e.state.Flag('+') {
+		// print stack trace for debugging
+		p.Print(e.err, "\n")
+		e.frame.Format(p)
+		e.chainStateAndVerb(e.nextErr)
+		return e.nextErr
+	}
+	err := e.nextErr
+	for {
+		if wrapErr, ok := err.(*wrapError); ok {
+			err = wrapErr.nextErr
+			continue
+		}
+		break
+	}
+	if ColoredErr {
+		var yp printer.Printer
+		p.Print(yp.PrintErrorMessage("syntax error: ", ColoredErr))
+	} else {
+		p.Print("syntax error: ")
+	}
+	e.chainStateAndVerb(err)
+	err.(xerrors.Formatter).FormatError(p)
+	return nil
+}
+
+type wrapState struct {
+	org fmt.State
+}
+
+func (s *wrapState) Write(b []byte) (n int, err error) {
+	return s.org.Write(b)
+}
+
+func (s *wrapState) Width() (wid int, ok bool) {
+	return s.org.Width()
+}
+
+func (s *wrapState) Precision() (prec int, ok bool) {
+	return s.org.Precision()
+}
+
+func (s *wrapState) Flag(c int) bool {
+	// set true to 'printDetail' forced because when p.Detail() is false, xerrors.Printer no output any text
+	if c == '#' {
+		// ignore '#' keyword because xerrors.FormatError doesn't set true to printDetail.
+		// ( see https://github.com/golang/xerrors/blob/master/adaptor.go#L39-L43 )
+		return false
+	}
+	return true
+}
+
+func (e *wrapError) Format(state fmt.State, verb rune) {
+	e.state = state
+	e.verb = verb
+	xerrors.FormatError(e, &wrapState{org: state}, verb)
+}
+
+func (e *wrapError) Error() string {
+	return e.err.Error()
+}
+
+type syntaxError struct {
+	*baseError
+	Msg   string
+	Token *token.Token
+	frame xerrors.Frame
+}
+
+func (e *syntaxError) FormatError(p xerrors.Printer) error {
+	if e.verb == 'v' && e.state.Flag('+') {
+		// %+v
+		// print stack trace for debugging
+		p.Print(e.Error())
+		e.frame.Format(p)
+	} else {
+		p.Print(e.Error())
+	}
+	return nil
+}
+
+func (e *syntaxError) Error() string {
+	var p printer.Printer
+	pos := fmt.Sprintf("[%d:%d] ", e.Token.Position.Line, e.Token.Position.Column)
+	msg := p.PrintErrorMessage(fmt.Sprintf("%s%s", pos, e.Msg), ColoredErr)
+	if WithSourceCode {
+		err := p.PrintErrorToken(e.Token, ColoredErr)
+		return fmt.Sprintf("%s\n%s", msg, err)
+	}
+	return msg
+}
diff --git a/parser/parser.go b/parser/parser.go
index 080fdcb..8190647 100644
--- a/parser/parser.go
+++ b/parser/parser.go
@@ -1,10 +1,11 @@
 package parser
 
 import (
+	"strings"
+
 	"github.com/goccy/go-yaml/ast"
+	"github.com/goccy/go-yaml/errors"
 	"github.com/goccy/go-yaml/token"
-
-	"golang.org/x/xerrors"
 )
 
 // Parser convert from token instances to ast
@@ -91,11 +92,11 @@ func (p *Parser) parseMapping(ctx *Context) (ast.Node, error) {
 
 		value, err := p.parseToken(ctx, tk)
 		if err != nil {
-			return nil, xerrors.Errorf("failed to parse mapping value in mapping node: %w", err)
+			return nil, errors.Wrapf(err, "failed to parse mapping value in mapping node")
 		}
 		mvnode, ok := value.(*ast.MappingValueNode)
 		if !ok {
-			return nil, xerrors.New("failed to parse flow mapping value node")
+			return nil, errors.ErrSyntax("failed to parse flow mapping value node", value.GetToken())
 		}
 		node.Values = append(node.Values, mvnode)
 		ctx.progress(1)
@@ -121,7 +122,7 @@ func (p *Parser) parseSequence(ctx *Context) (ast.Node, error) {
 
 		value, err := p.parseToken(ctx, tk)
 		if err != nil {
-			return nil, xerrors.Errorf("failed to parse sequence value in flow sequence node: %w", err)
+			return nil, errors.Wrapf(err, "failed to parse sequence value in flow sequence node")
 		}
 		node.Values = append(node.Values, value)
 		ctx.progress(1)
@@ -134,7 +135,7 @@ func (p *Parser) parseTag(ctx *Context) (ast.Node, error) {
 	ctx.progress(1) // skip tag token
 	value, err := p.parseToken(ctx, ctx.currentToken())
 	if err != nil {
-		return nil, xerrors.Errorf("failed to parse tag value: %w", err)
+		return nil, errors.Wrapf(err, "failed to parse tag value")
 	}
 	node.Value = value
 	return node, nil
@@ -147,20 +148,34 @@ func (p *Parser) isMapNode(node ast.Node) bool {
 	return false
 }
 
+func (p *Parser) validateMapKey(tk *token.Token) error {
+	if tk.Type != token.StringType {
+		return nil
+	}
+	origin := strings.TrimLeft(tk.Origin, "\n")
+	if strings.Index(origin, "\n") > 0 {
+		return errors.ErrSyntax("unexpected key name", tk)
+	}
+	return nil
+}
+
 func (p *Parser) parseMappingValue(ctx *Context) (ast.Node, error) {
 	key := p.parseMapKey(ctx.currentToken())
 	if key == nil {
-		return nil, xerrors.New("failed to parse mapping 'key'. key is undefined")
+		return nil, errors.ErrSyntax("unexpected mapping 'key'. key is undefined", ctx.currentToken())
+	}
+	if err := p.validateMapKey(key.GetToken()); err != nil {
+		return nil, errors.Wrapf(err, "validate mapping key error")
 	}
 	if _, ok := key.(ast.ScalarNode); !ok {
-		return nil, xerrors.New("failed to parse mapping 'key', key is not scalar node")
+		return nil, errors.ErrSyntax("unexpected mapping 'key', key is not scalar value", key.GetToken())
 	}
 	ctx.progress(1)          // progress to mapping value token
 	tk := ctx.currentToken() // get mapping value token
 	ctx.progress(1)          // progress to value token
 	value, err := p.parseToken(ctx, ctx.currentToken())
 	if err != nil {
-		return nil, xerrors.Errorf("failed to parse mapping 'value' node: %w", err)
+		return nil, errors.Wrapf(err, "failed to parse mapping 'value' node")
 	}
 	keyColumn := key.GetToken().Position.Column
 	valueColumn := value.GetToken().Position.Column
@@ -176,10 +191,10 @@ func (p *Parser) parseMappingValue(ctx *Context) (ast.Node, error) {
 			ctx.progress(1)
 			value, err := p.parseToken(ctx, ctx.currentToken())
 			if err != nil {
-				return nil, xerrors.Errorf("failed to parse mapping value node: %w", err)
+				return nil, errors.Wrapf(err, "failed to parse mapping value node")
 			}
 			if !p.isMapNode(value) {
-				return nil, xerrors.Errorf("failed to parse mapping value node")
+				return nil, errors.Wrapf(err, "failed to parse mapping value node")
 			}
 			node.Values = append(node.Values, value)
 			nextKeyToken := ctx.nextToken()
@@ -203,6 +218,14 @@ func (p *Parser) parseMappingValue(ctx *Context) (ast.Node, error) {
 			Value: node,
 		}, nil
 	}
+	if keyColumn == valueColumn {
+		if value.Type() == ast.StringType {
+			ntk := ctx.nextToken()
+			if ntk == nil || (ntk.Type != token.MappingValueType && ntk.Type != token.SequenceEntryType) {
+				return nil, errors.ErrSyntax("could not found expected ':' token", value.GetToken())
+			}
+		}
+	}
 	mvnode := &ast.MappingValueNode{
 		Start: tk,
 		Key:   key,
@@ -219,9 +242,15 @@ func (p *Parser) parseMappingValue(ctx *Context) (ast.Node, error) {
 		ctx.progress(1)
 		value, err := p.parseToken(ctx, ctx.currentToken())
 		if err != nil {
-			return nil, xerrors.Errorf("failed to parse mapping collection node: %w", err)
+			return nil, errors.Wrapf(err, "failed to parse mapping collection node")
+		}
+		if c, ok := value.(*ast.MappingCollectionNode); ok {
+			for _, v := range c.Values {
+				node.Values = append(node.Values, v)
+			}
+		} else {
+			node.Values = append(node.Values, value)
 		}
-		node.Values = append(node.Values, value)
 		ntk = ctx.nextToken()
 		antk = ctx.afterNextToken()
 		return node, nil
@@ -240,7 +269,7 @@ func (p *Parser) parseSequenceEntry(ctx *Context) (ast.Node, error) {
 		ctx.progress(1) // skip sequence token
 		value, err := p.parseToken(ctx, ctx.currentToken())
 		if err != nil {
-			return nil, xerrors.Errorf("failed to parse sequence: %w", err)
+			return nil, errors.Wrapf(err, "failed to parse sequence")
 		}
 		sequenceNode.Values = append(sequenceNode.Values, value)
 		tk = ctx.nextToken()
@@ -263,22 +292,22 @@ func (p *Parser) parseAnchor(ctx *Context) (ast.Node, error) {
 	anchor := &ast.AnchorNode{Start: tk}
 	ntk := ctx.nextToken()
 	if ntk == nil {
-		return nil, xerrors.New("failed to parse anchor. anchor name is undefined")
+		return nil, errors.ErrSyntax("unexpected anchor. anchor name is undefined", tk)
 	}
 	ctx.progress(1) // skip anchor token
 	name, err := p.parseToken(ctx, ctx.currentToken())
 	if err != nil {
-		return nil, xerrors.Errorf("failed to parser anchor name node: %w", err)
+		return nil, errors.Wrapf(err, "failed to parser anchor name node")
 	}
 	anchor.Name = name
 	ntk = ctx.nextToken()
 	if ntk == nil {
-		return nil, xerrors.New("failed to parse anchor. anchor value is undefined")
+		return nil, errors.ErrSyntax("unexpected anchor. anchor value is undefined", ctx.currentToken())
 	}
 	ctx.progress(1)
 	value, err := p.parseToken(ctx, ctx.currentToken())
 	if err != nil {
-		return nil, xerrors.Errorf("failed to parser anchor name node: %w", err)
+		return nil, errors.Wrapf(err, "failed to parser anchor name node")
 	}
 	anchor.Value = value
 	return anchor, nil
@@ -289,12 +318,12 @@ func (p *Parser) parseAlias(ctx *Context) (ast.Node, error) {
 	alias := &ast.AliasNode{Start: tk}
 	ntk := ctx.nextToken()
 	if ntk == nil {
-		return nil, xerrors.New("failed to parse alias. alias name is undefined")
+		return nil, errors.ErrSyntax("unexpected alias. alias name is undefined", tk)
 	}
 	ctx.progress(1) // skip alias token
 	name, err := p.parseToken(ctx, ctx.currentToken())
 	if err != nil {
-		return nil, xerrors.Errorf("failed to parser alias name node: %w", err)
+		return nil, errors.Wrapf(err, "failed to parser alias name node")
 	}
 	alias.Value = name
 	return alias, nil
@@ -346,12 +375,12 @@ func (p *Parser) parseDirective(ctx *Context) (ast.Node, error) {
 	ctx.progress(1) // skip directive token
 	value, err := p.parseToken(ctx, ctx.currentToken())
 	if err != nil {
-		return nil, xerrors.Errorf("failed to parse directive value: %w", err)
+		return nil, errors.Wrapf(err, "failed to parse directive value")
 	}
 	node.Value = value
 	ctx.progress(1)
 	if ctx.currentToken().Type != token.DocumentHeaderType {
-		return nil, xerrors.New("failed to parse directive value. document not started")
+		return nil, errors.ErrSyntax("unexpected directive value. document not started", ctx.currentToken())
 	}
 	return node, nil
 }
@@ -361,11 +390,11 @@ func (p *Parser) parseLiteral(ctx *Context) (ast.Node, error) {
 	ctx.progress(1) // skip literal/folded token
 	value, err := p.parseToken(ctx, ctx.currentToken())
 	if err != nil {
-		return nil, xerrors.Errorf("failed to parse literal/folded value: %w", err)
+		return nil, errors.Wrapf(err, "failed to parse literal/folded value")
 	}
 	snode, ok := value.(*ast.StringNode)
 	if !ok {
-		return nil, xerrors.New("invalid literal type. required string node")
+		return nil, errors.ErrSyntax("unexpected token. required string token", value.GetToken())
 	}
 	node.Value = snode
 	return node, nil
@@ -406,7 +435,7 @@ func (p *Parser) Parse(tokens token.Tokens) (*ast.Document, error) {
 	for ctx.next() {
 		node, err := p.parseToken(ctx, ctx.currentToken())
 		if err != nil {
-			return nil, xerrors.Errorf("failed to parse: %w", err)
+			return nil, errors.Wrapf(err, "failed to parse")
 		}
 		ctx.progress(1)
 		if node == nil {
diff --git a/printer/printer.go b/printer/printer.go
index 615a786..5b64e10 100644
--- a/printer/printer.go
+++ b/printer/printer.go
@@ -2,8 +2,10 @@ package printer
 
 import (
 	"fmt"
+	"math"
 	"strings"
 
+	"github.com/fatih/color"
 	"github.com/goccy/go-yaml/ast"
 	"github.com/goccy/go-yaml/token"
 )
@@ -100,13 +102,16 @@ func (p *Printer) property(tk *token.Token) *Property {
 
 // PrintTokens create text from token collection
 func (p *Printer) PrintTokens(tokens token.Tokens) string {
+	if len(tokens) == 0 {
+		return ""
+	}
 	if p.LineNumber {
 		if p.LineNumberFormat == nil {
 			p.LineNumberFormat = defaultLineNumberFormat
 		}
 	}
 	texts := []string{}
-	lineNumber := 1
+	lineNumber := tokens[0].Position.Line
 	for _, tk := range tokens {
 		lines := strings.Split(tk.Origin, "\n")
 		prop := p.property(tk)
@@ -151,3 +156,123 @@ func (p *Printer) PrintTokens(tokens token.Tokens) string {
 func (p *Printer) PrintNode(node ast.Node) []byte {
 	return []byte(fmt.Sprintf("%+v\n", node))
 }
+
+const escape = "\x1b"
+
+func format(attr color.Attribute) string {
+	return fmt.Sprintf("%s[%dm", escape, attr)
+}
+
+func (p *Printer) setDefaultColorSet() {
+	p.Bool = func() *Property {
+		return &Property{
+			Prefix: format(color.FgHiMagenta),
+			Suffix: format(color.Reset),
+		}
+	}
+	p.Number = func() *Property {
+		return &Property{
+			Prefix: format(color.FgHiMagenta),
+			Suffix: format(color.Reset),
+		}
+	}
+	p.MapKey = func() *Property {
+		return &Property{
+			Prefix: format(color.FgHiCyan),
+			Suffix: format(color.Reset),
+		}
+	}
+	p.Anchor = func() *Property {
+		return &Property{
+			Prefix: format(color.FgHiYellow),
+			Suffix: format(color.Reset),
+		}
+	}
+	p.Alias = func() *Property {
+		return &Property{
+			Prefix: format(color.FgHiYellow),
+			Suffix: format(color.Reset),
+		}
+	}
+	p.String = func() *Property {
+		return &Property{
+			Prefix: format(color.FgHiGreen),
+			Suffix: format(color.Reset),
+		}
+	}
+}
+
+func (p *Printer) PrintErrorMessage(msg string, isColored bool) string {
+	if isColored {
+		return fmt.Sprintf("%s%s%s",
+			format(color.FgHiRed),
+			msg,
+			format(color.Reset),
+		)
+	}
+	return msg
+}
+
+func (p *Printer) PrintErrorToken(tk *token.Token, isColored bool) string {
+	errToken := tk
+	pos := tk.Position
+	curLine := pos.Line
+	curExtLine := curLine + len(strings.Split(tk.Origin, "\n")) - 1
+	minLine := int(math.Max(float64(curLine-3), 1))
+	maxLine := curExtLine + 3
+	for {
+		if tk.Position.Line < minLine {
+			break
+		}
+		if tk.Prev == nil {
+			break
+		}
+		tk = tk.Prev
+	}
+	tokens := token.Tokens{}
+	lastTk := tk
+	for tk.Position.Line <= curExtLine {
+		tokens.Add(tk)
+		lastTk = tk
+		tk = tk.Next
+		if tk == nil {
+			break
+		}
+	}
+	org := lastTk.Origin
+	trimmed := strings.TrimRight(strings.TrimRight(lastTk.Origin, " "), "\n")
+	lastTk.Origin = trimmed
+	if tk != nil {
+		tk.Origin = org[len(org)-len(trimmed):] + tk.Origin
+	}
+	p.LineNumber = true
+	p.LineNumberFormat = func(num int) string {
+		if isColored {
+			fn := color.New(color.Bold, color.FgHiWhite).SprintFunc()
+			if curLine == num {
+				return fn(fmt.Sprintf("> %2d | ", num))
+			}
+			return fn(fmt.Sprintf("  %2d | ", num))
+		}
+		if curLine == num {
+			return fmt.Sprintf("> %2d | ", num)
+		}
+		return fmt.Sprintf("  %2d | ", num)
+	}
+	if isColored {
+		p.setDefaultColorSet()
+	}
+	beforeSource := p.PrintTokens(tokens)
+	prefixSpaceNum := len(fmt.Sprintf("  %2d | ", 1))
+	annotateLine := strings.Repeat(" ", prefixSpaceNum+errToken.Position.Column-2) + "^"
+	tokens = token.Tokens{}
+	for tk != nil {
+		if tk.Position.Line > maxLine {
+			break
+		}
+		tokens.Add(tk)
+		tk = tk.Next
+	}
+	afterSource := p.PrintTokens(tokens)
+	return fmt.Sprintf("%s\n%s\n%s", beforeSource, annotateLine, afterSource)
+}
diff --git a/scanner/scanner.go b/scanner/scanner.go
index 57a225b..295a77e 100644
--- a/scanner/scanner.go
+++ b/scanner/scanner.go
@@ -1,7 +1,6 @@
 package scanner
 
 import (
-	"fmt"
 	"io"
 	"strings"
 
@@ -315,7 +314,6 @@ func (s *Scanner) scan(ctx *Context) (pos int) {
 		case '<':
 			if ctx.repeatNum('<') == 2 {
 				s.prevIndentKeyNum = s.column
-				fmt.Println("s.prevIndentKeyNum = ", s.prevIndentKeyNum)
 				ctx.addToken(token.MergeKey(string(ctx.obuf)+"<<", s.pos()))
 				s.progressColumn(ctx, 1)
 				pos++
@@ -460,6 +458,7 @@ func (s *Scanner) scan(ctx *Context) (pos int) {
 		ctx.addOriginBuf(c)
 		s.progressColumn(ctx, 1)
 	}
+	s.addBufferedTokenIfExists(ctx)
 	return
 }
 


Test patch:
diff --git a/decode_test.go b/decode_test.go
index b653697..b0e3209 100644
--- a/decode_test.go
+++ b/decode_test.go
@@ -286,7 +286,7 @@ func TestDecoder(t *testing.T) {
 		typ := reflect.ValueOf(test.value).Type()
 		value := reflect.New(typ)
 		if err := dec.Decode(value.Interface()); err != nil {
-			t.Fatalf("%+v", err)
+			t.Fatalf("%s: %+v", test.source, err)
 		}
 		actual := fmt.Sprintf("%v", value.Elem().Interface())
 		expect := fmt.Sprintf("%v", test.value)
diff --git a/parser/parser_test.go b/parser/parser_test.go
index 45f25af..648c270 100644
--- a/parser/parser_test.go
+++ b/parser/parser_test.go
@@ -328,7 +328,6 @@ aliased: *anchor
 		var (
 			p parser.Parser
 		)
-		//fmt.Printf(test.source)
 		tokens := lexer.Tokenize(test.source)
 		tokens.Dump()
 		doc, err := p.Parse(tokens)
@@ -346,6 +345,23 @@ aliased: *anchor
 	}
 }
 
+func TestSyntaxError(t *testing.T) {
+	sources := []string{
+		"a:\n- b\n  c: d\n  e: f\n  g: h",
+	}
+	for _, source := range sources {
+		var (
+			p parser.Parser
+		)
+		tokens := lexer.Tokenize(source)
+		_, err := p.Parse(tokens)
+		if err == nil {
+			t.Fatal("cannot catch syntax error")
+		}
+		fmt.Printf("%v\n", err)
+	}
+}
+
 type Visitor struct {
 }