diff options
author | Ryo Nihei <nihei.dev@gmail.com> | 2021-07-17 17:29:31 +0900 |
---|---|---|
committer | Ryo Nihei <nihei.dev@gmail.com> | 2021-07-17 17:29:31 +0900 |
commit | 2c22c1e193ce8b3abd6f1436457ff92a40646e45 (patch) | |
tree | 2979b4d644e038ae2ec84a201c2557d3db5e1570 | |
parent | Improve syntax error messages (diff) | |
download | urubu-2c22c1e193ce8b3abd6f1436457ff92a40646e45.tar.gz urubu-2c22c1e193ce8b3abd6f1436457ff92a40646e45.tar.xz |
Detect multiple syntax errors in a single parse
-rw-r--r-- | cmd/vartan/compile.go | 27 | ||||
-rw-r--r-- | error/error.go | 16 | ||||
-rw-r--r-- | grammar/grammar.go | 4 | ||||
-rw-r--r-- | spec/parser.go | 109 | ||||
-rw-r--r-- | spec/parser_test.go | 8 | ||||
-rw-r--r-- | spec/syntax_error.go | 1 |
6 files changed, 126 insertions, 39 deletions
diff --git a/cmd/vartan/compile.go b/cmd/vartan/compile.go index 3601f00..804bd4f 100644 --- a/cmd/vartan/compile.go +++ b/cmd/vartan/compile.go @@ -54,14 +54,16 @@ func runCompile(cmd *cobra.Command, args []string) (retErr error) { } if retErr != nil { - specErr, ok := retErr.(*verr.SpecError) + specErrs, ok := retErr.(verr.SpecErrors) if ok { - if *compileFlags.grammar != "" { - specErr.FilePath = grmPath - specErr.SourceName = grmPath - } else { - specErr.FilePath = grmPath - specErr.SourceName = "stdin" + for _, err := range specErrs { + if *compileFlags.grammar != "" { + err.FilePath = grmPath + err.SourceName = grmPath + } else { + err.FilePath = grmPath + err.SourceName = "stdin" + } } } fmt.Fprintln(os.Stderr, retErr) @@ -106,24 +108,17 @@ func runCompile(cmd *cobra.Command, args []string) (retErr error) { } func readGrammar(path string) (grm *grammar.Grammar, retErr error) { - defer func() { - err := recover() - specErr, ok := err.(*verr.SpecError) - if ok { - specErr.FilePath = path - retErr = specErr - } - }() - f, err := os.Open(path) if err != nil { return nil, fmt.Errorf("Cannot open the grammar file %s: %w", path, err) } defer f.Close() + ast, err := spec.Parse(f) if err != nil { return nil, err } + return grammar.NewGrammar(ast) } diff --git a/error/error.go b/error/error.go index cc4fed5..1e5df7a 100644 --- a/error/error.go +++ b/error/error.go @@ -7,6 +7,22 @@ import ( "strings" ) +type SpecErrors []*SpecError + +func (e SpecErrors) Error() string { + if len(e) == 0 { + return "" + } + + var b strings.Builder + fmt.Fprintf(&b, "%v", e[0]) + for _, err := range e[1:] { + fmt.Fprintf(&b, "\n%v", err) + } + + return b.String() +} + type SpecError struct { Cause error FilePath string diff --git a/grammar/grammar.go b/grammar/grammar.go index b9f65dc..b4827df 100644 --- a/grammar/grammar.go +++ b/grammar/grammar.go @@ -144,6 +144,10 @@ func NewGrammar(root *spec.RootNode) (*Grammar, error) { } } + if len(root.Productions) == 0 { + return nil, fmt.Errorf("a grammar must have at least one production") + } + prods := newProductionSet() var augStartSym symbol astActs := map[productionID][]*astActionEntry{} diff --git a/spec/parser.go b/spec/parser.go index 1bd2fb4..8b825bf 100644 --- a/spec/parser.go +++ b/spec/parser.go @@ -1,6 +1,7 @@ package spec import ( + "fmt" "io" verr "github.com/nihei9/vartan/error" @@ -64,17 +65,15 @@ func Parse(src io.Reader) (*RootNode, error) { if err != nil { return nil, err } - root, err := p.parse() - if err != nil { - return nil, err - } - return root, nil + + return p.parse() } type parser struct { lex *lexer peekedTok *token lastTok *token + errs verr.SpecErrors // A token position that the parser read at last. // It is used as additional information in error messages. @@ -92,22 +91,29 @@ func newParser(src io.Reader) (*parser, error) { } func (p *parser) parse() (root *RootNode, retErr error) { + root = p.parseRoot() + if len(p.errs) > 0 { + return nil, p.errs + } + + return root, nil +} + +func (p *parser) parseRoot() *RootNode { defer func() { err := recover() if err != nil { - retErr = err.(error) - return + specErr, ok := err.(*verr.SpecError) + if !ok { + panic(fmt.Errorf("an unexpected error occurred: %v", err)) + } + p.errs = append(p.errs, specErr) } }() - return p.parseRoot(), nil -} -func (p *parser) parseRoot() *RootNode { var prods []*ProductionNode var fragments []*FragmentNode for { - p.consume(tokenKindNewline) - fragment := p.parseFragment() if fragment != nil { fragments = append(fragments, fragment) @@ -120,10 +126,9 @@ func (p *parser) parseRoot() *RootNode { continue } - break - } - if len(prods) == 0 { - raiseSyntaxError(0, synErrNoProduction) + if p.consume(tokenKindEOF) { + break + } } return &RootNode{ @@ -133,6 +138,25 @@ func (p *parser) parseRoot() *RootNode { } func (p *parser) parseFragment() *FragmentNode { + defer func() { + err := recover() + if err == nil { + return + } + + specErr, ok := err.(*verr.SpecError) + if !ok { + panic(err) + } + + p.errs = append(p.errs, specErr) + p.skipOverTo(tokenKindSemicolon) + + return + }() + + p.consume(tokenKindNewline) + if !p.consume(tokenKindKWFragment) { return nil } @@ -174,6 +198,25 @@ func (p *parser) parseFragment() *FragmentNode { } func (p *parser) parseProduction() *ProductionNode { + defer func() { + err := recover() + if err == nil { + return + } + + specErr, ok := err.(*verr.SpecError) + if !ok { + panic(err) + } + + p.errs = append(p.errs, specErr) + p.skipOverTo(tokenKindSemicolon) + + return + }() + + p.consume(tokenKindNewline) + if p.consume(tokenKindEOF) { return nil } @@ -351,3 +394,37 @@ func (p *parser) consume(expected tokenKind) bool { return false } + +func (p *parser) skip() { + var tok *token + var err error + for { + if p.peekedTok != nil { + tok = p.peekedTok + p.peekedTok = nil + } else { + tok, err = p.lex.next() + if err != nil { + p.errs = append(p.errs, &verr.SpecError{ + Cause: err, + Row: p.pos.row, + }) + continue + } + } + + break + } + + p.lastTok = tok + p.pos = tok.pos +} + +func (p *parser) skipOverTo(kind tokenKind) { + for { + if p.consume(kind) || p.consume(tokenKindEOF) { + return + } + p.skip() + } +} diff --git a/spec/parser_test.go b/spec/parser_test.go index 8500a02..89cc4d1 100644 --- a/spec/parser_test.go +++ b/spec/parser_test.go @@ -144,11 +144,6 @@ c: ; synErr: synErrInvalidToken, }, { - caption: "a grammar must have at least one production", - src: ``, - synErr: synErrNoProduction, - }, - { caption: "a production must have its name as the first element", src: `: "a";`, synErr: synErrNoProductionName, @@ -350,10 +345,11 @@ foo: "foo"; t.Run(tt.caption, func(t *testing.T) { ast, err := Parse(strings.NewReader(tt.src)) if tt.synErr != nil { - synErr, ok := err.(*verr.SpecError) + synErrs, ok := err.(verr.SpecErrors) if !ok { t.Fatalf("unexpected error; want: %v, got: %v", tt.synErr, err) } + synErr := synErrs[0] if tt.synErr != synErr.Cause { t.Fatalf("unexpected error; want: %v, got: %v", tt.synErr, synErr.Cause) } diff --git a/spec/syntax_error.go b/spec/syntax_error.go index 041486d..9d67ccc 100644 --- a/spec/syntax_error.go +++ b/spec/syntax_error.go @@ -24,7 +24,6 @@ var ( // syntax errors synErrInvalidToken = newSyntaxError("invalid token") - synErrNoProduction = newSyntaxError("a grammar must have at least one production") synErrNoProductionName = newSyntaxError("a production name is missing") synErrNoColon = newSyntaxError("the colon must precede alternatives") synErrNoSemicolon = newSyntaxError("the semicolon is missing at the last of an alternative") |