[FAILING] error reporting, part

This commit is contained in:
Arpad Ryszka 2017-11-25 17:37:05 +01:00
parent 062ad5b046
commit 41fdcbdef0
8 changed files with 231 additions and 35 deletions

View File

@ -58,7 +58,7 @@ func (p *charParser) match(t rune) bool {
func (p *charParser) parse(c *context) { func (p *charParser) parse(c *context) {
if tok, ok := c.token(); !ok || !p.match(tok) { if tok, ok := c.token(); !ok || !p.match(tok) {
c.fail(c.offset) c.fail(p, c.offset)
return return
} }

View File

@ -133,14 +133,15 @@ func (d *choiceDefinition) builder() builder { return d.cbuilder }
func (p *choiceParser) nodeName() string { return p.name } func (p *choiceParser) nodeName() string { return p.name }
func (p *choiceParser) nodeID() int { return p.id } func (p *choiceParser) nodeID() int { return p.id }
func (p *choiceParser) commitType() CommitType { return p.commit }
func (p *choiceParser) parse(c *context) { func (p *choiceParser) parse(c *context) {
if c.fromResults(p.id) { if c.fromResults(p) {
return return
} }
if c.results.pending(c.offset, p.id) { if c.results.pending(c.offset, p.id) {
c.fail(c.offset) c.fail(p, c.offset)
return return
} }
@ -185,7 +186,7 @@ func (p *choiceParser) parse(c *context) {
} }
c.results.setNoMatch(from, p.id) c.results.setNoMatch(from, p.id)
c.fail(from) c.fail(p, from)
c.results.unmarkPending(from, p.id) c.results.unmarkPending(from, p.id)
} }

View File

@ -9,6 +9,9 @@ type context struct {
reader io.RuneReader reader io.RuneReader
offset int offset int
readOffset int readOffset int
consumed int
failOffset int
failingParser parser
readErr error readErr error
eof bool eof bool
results *results results *results
@ -62,8 +65,8 @@ func (c *context) token() (rune, bool) {
return c.tokens[c.offset], true return c.tokens[c.offset], true
} }
func (c *context) fromResults(id int) bool { func (c *context) fromResults(p parser) bool {
to, m, ok := c.results.longestResult(c.offset, id) to, m, ok := c.results.longestResult(c.offset, p.nodeID())
if !ok { if !ok {
return false return false
} }
@ -71,7 +74,7 @@ func (c *context) fromResults(id int) bool {
if m { if m {
c.success(to) c.success(to)
} else { } else {
c.fail(c.offset) c.fail(p, c.offset)
} }
return true return true
@ -80,21 +83,66 @@ func (c *context) fromResults(id int) bool {
func (c *context) success(to int) { func (c *context) success(to int) {
c.offset = to c.offset = to
c.matchLast = true c.matchLast = true
if to > c.consumed {
c.consumed = to
}
} }
func (c *context) fail(offset int) { func (c *context) fail(p parser, offset int) {
c.offset = offset c.offset = offset
c.matchLast = false c.matchLast = false
if c.failingParser == nil || c.consumed > c.failOffset {
// TODO: choice can be retried
println("setting fail", p.nodeName(), c.failingParser == nil, c.failOffset, c.consumed)
c.failOffset = c.consumed
if p.commitType()&userDefined != 0 {
c.failingParser = p
}
}
} }
func (c *context) finalizeParse(rootID int) error { func findLine(tokens []rune, offset int) (line, column int) {
if !c.matchLast { tokens = tokens[:offset]
return ErrInvalidInput for i := range tokens {
column++
if tokens[i] == '\n' {
column = 0
line++
}
} }
to, match, found := c.results.longestResult(0, rootID) return
}
func (c *context) parseError(root parser) error {
definition := root.nodeName()
if c.failingParser == nil {
println("setting fail", c.failOffset, c.consumed)
c.failOffset = c.consumed
} else {
definition = c.failingParser.nodeName()
}
line, col := findLine(c.tokens, c.failOffset)
return &ParseError{
Offset: c.failOffset,
Line: line,
Column: col,
Definition: definition,
}
}
func (c *context) finalizeParse(root parser) error {
if !c.matchLast {
return c.parseError(root)
}
to, match, found := c.results.longestResult(0, root.nodeID())
// TODO: test all three cases
if !found || !match || to < c.readOffset { if !found || !match || to < c.readOffset {
return ErrUnexpectedCharacter return c.parseError(root)
} }
if !c.eof { if !c.eof {
@ -104,7 +152,7 @@ func (c *context) finalizeParse(rootID int) error {
return c.readErr return c.readErr
} }
return ErrUnexpectedCharacter return c.parseError(root)
} }
} }

View File

@ -191,7 +191,7 @@ func defineDefinition(s *Syntax, n *Node) error {
return defineExpression( return defineExpression(
s, s,
n.Nodes[0].Text(), n.Nodes[0].Text(),
flagsToCommitType(n.Nodes[1:len(n.Nodes)-1]), flagsToCommitType(n.Nodes[1:len(n.Nodes)-1])|userDefined,
n.Nodes[len(n.Nodes)-1], n.Nodes[len(n.Nodes)-1],
) )
} }

115
errors_test.go Normal file
View File

@ -0,0 +1,115 @@
package treerack
import (
"bytes"
"testing"
)
func TestError(t *testing.T) {
type testItem struct {
title string
syntax string
text string
offset int
line int
column int
definition string
}
for _, test := range []testItem{{
title: "single def, empty text",
syntax: `a = "a"`,
definition: "a",
}, {
title: "single def, wrong text",
syntax: `a = "a"`,
text: "b",
definition: "a",
}, {
title: "single optional def, wrong text",
syntax: `a = "a"?`,
text: "b",
definition: "a",
}, {
title: "error on second line, second column",
syntax: `a = [a\n]*`,
text: "aa\nabaa\naa",
offset: 4,
line: 1,
column: 1,
definition: "a",
}, {
title: "multiple definitions",
syntax: `a = "aa"; A:root = a`,
text: "ab",
offset: 1,
column: 1,
definition: "a",
}, {
title: "choice, options succeed",
syntax: `a = "12"; b = "1"; c:root = a | b`,
text: "123",
offset: 2,
column: 2,
definition: "c",
}, {
title: "choice, longer option fails",
syntax: `a = "12"; b = "1"; c:root = a | b`,
text: "13",
offset: 1,
column: 1,
definition: "a",
}, {
title: "choice, shorter option fails",
syntax: `a = "2"; b = "12"; c:root = a | b`,
text: "123",
offset: 0,
column: 0,
definition: "1",
}, {
title: "choice, both options fail",
syntax: `a = "12"; b = "2"; c:root = a | b`,
text: "13",
offset: 1,
column: 1,
definition: "a",
}} {
t.Run(test.title, func(t *testing.T) {
s, err := openSyntaxString(test.syntax)
if err != nil {
t.Error(err)
return
}
_, err = s.Parse(bytes.NewBufferString(test.text))
if err == nil {
t.Error("failed to fail")
return
}
perr, ok := err.(*ParseError)
if !ok {
t.Error("invalid error returned", err)
return
}
if perr.Offset != test.offset {
t.Error("invalid error offset", perr.Offset, test.offset)
return
}
if perr.Line != test.line {
t.Error("invalid line index", perr.Line, test.line)
return
}
if perr.Column != test.column {
t.Error("invalid column index", perr.Column, test.column)
}
if perr.Definition != test.definition {
t.Error("invalid definition", perr.Definition, test.definition)
}
})
}
}

View File

@ -17,6 +17,7 @@ func openSyntaxReader(r io.Reader) (*Syntax, error) {
return nil, err return nil, err
} }
println("starting")
s := &Syntax{} s := &Syntax{}
if err := define(s, doc); err != nil { if err := define(s, doc); err != nil {
return nil, err return nil, err

View File

@ -170,11 +170,12 @@ func (d *sequenceDefinition) builder() builder { return d.sbuilder }
func (p *sequenceParser) nodeName() string { return p.name } func (p *sequenceParser) nodeName() string { return p.name }
func (p *sequenceParser) nodeID() int { return p.id } func (p *sequenceParser) nodeID() int { return p.id }
func (p *sequenceParser) commitType() CommitType { return p.commit }
func (p *sequenceParser) parse(c *context) { func (p *sequenceParser) parse(c *context) {
if !p.allChars { if !p.allChars {
if c.results.pending(c.offset, p.id) { if c.results.pending(c.offset, p.id) {
c.fail(c.offset) c.fail(p, c.offset)
return return
} }
@ -191,7 +192,7 @@ func (p *sequenceParser) parse(c *context) {
p.items[itemIndex].parse(c) p.items[itemIndex].parse(c)
if !c.matchLast { if !c.matchLast {
if currentCount < p.ranges[itemIndex][0] { if currentCount < p.ranges[itemIndex][0] {
c.fail(from) c.fail(p, from)
if !p.allChars { if !p.allChars {
c.results.unmarkPending(from, p.id) c.results.unmarkPending(from, p.id)
} }

View File

@ -15,6 +15,8 @@ const (
Whitespace Whitespace
NoWhitespace NoWhitespace
Root Root
userDefined
) )
// if min=0&&max=0, it means min=1,max=1 // if min=0&&max=0, it means min=1,max=1
@ -25,6 +27,29 @@ type SequenceItem struct {
Min, Max int Min, Max int
} }
// ParseError is returned when the input text doesn't match
// the used syntax during parsing.
type ParseError struct {
// Offset is the index of the right-most failing
// token in the input text.
Offset int
// Line tells the line index of the right-most failing
// token in the input text.
//
// It is zero-based, and for error reporting, it is
// recommended to increment it by one.
Line int
// Column tells the column index of the right-most failing
// token in the input text.
Column int
// Definition tells the right-most unmatched parser definition.
Definition string
}
type Syntax struct { type Syntax struct {
registry *registry registry *registry
initialized bool initialized bool
@ -53,6 +78,7 @@ type definition interface {
type parser interface { type parser interface {
nodeName() string nodeName() string
nodeID() int nodeID() int
commitType() CommitType
parse(*context) parse(*context)
} }
@ -117,6 +143,10 @@ func intsContain(is []int, i int) bool {
return false return false
} }
func (pe *ParseError) Error() string {
return "parse error"
}
func (s *Syntax) applyRoot(d definition) error { func (s *Syntax) applyRoot(d definition) error {
explicitRoot := d.commitType()&Root != 0 explicitRoot := d.commitType()&Root != 0
if explicitRoot && s.explicitRoot { if explicitRoot && s.explicitRoot {
@ -164,7 +194,7 @@ func (s *Syntax) AnyChar(name string, ct CommitType) error {
return ErrInvalidSymbolName return ErrInvalidSymbolName
} }
return s.anyChar(name, ct) return s.anyChar(name, ct|userDefined)
} }
func childName(name string, childIndex int) string { func childName(name string, childIndex int) string {
@ -194,7 +224,7 @@ func (s *Syntax) Class(name string, ct CommitType, not bool, chars []rune, range
return ErrInvalidSymbolName return ErrInvalidSymbolName
} }
return s.class(name, ct, not, chars, ranges) return s.class(name, ct|userDefined, not, chars, ranges)
} }
func (s *Syntax) charSequence(name string, ct CommitType, chars []rune) error { func (s *Syntax) charSequence(name string, ct CommitType, chars []rune) error {
@ -215,7 +245,7 @@ func (s *Syntax) CharSequence(name string, ct CommitType, chars []rune) error {
return ErrInvalidSymbolName return ErrInvalidSymbolName
} }
return s.charSequence(name, ct, chars) return s.charSequence(name, ct|userDefined, chars)
} }
func (s *Syntax) sequence(name string, ct CommitType, items ...SequenceItem) error { func (s *Syntax) sequence(name string, ct CommitType, items ...SequenceItem) error {
@ -229,7 +259,7 @@ func (s *Syntax) Sequence(name string, ct CommitType, items ...SequenceItem) err
return ErrInvalidSymbolName return ErrInvalidSymbolName
} }
return s.sequence(name, ct, items...) return s.sequence(name, ct|userDefined, items...)
} }
func (s *Syntax) choice(name string, ct CommitType, options ...string) error { func (s *Syntax) choice(name string, ct CommitType, options ...string) error {
@ -241,7 +271,7 @@ func (s *Syntax) Choice(name string, ct CommitType, options ...string) error {
return ErrInvalidSymbolName return ErrInvalidSymbolName
} }
return s.choice(name, ct, options...) return s.choice(name, ct|userDefined, options...)
} }
func (s *Syntax) Read(r io.Reader) error { func (s *Syntax) Read(r io.Reader) error {
@ -315,7 +345,7 @@ func (s *Syntax) Parse(r io.Reader) (*Node, error) {
return nil, c.readErr return nil, c.readErr
} }
if err := c.finalizeParse(s.parser.nodeID()); err != nil { if err := c.finalizeParse(s.parser); err != nil {
return nil, err return nil, err
} }