diff --git a/syntax/parser.go b/syntax/parser.go index df2b254d9..298d93139 100644 --- a/syntax/parser.go +++ b/syntax/parser.go @@ -314,6 +314,11 @@ type Parser struct { litBs []byte } +// Incomplete reports whether the parser is waiting to read more bytes because +// it needs to finish properly parsing a statement. +// +// It is only safe to call while the parser is blocked on a read. For an example +// use case, see the documentation for Parser.Interactive. func (p *Parser) Incomplete() bool { // If we're in a quote state other than noState, we're parsing a node // such as a double-quoted string. @@ -642,12 +647,22 @@ func (p *Parser) errPass(err error) { } } +// IsIncomplete reports whether a Parser error could have been avoided with +// extra input bytes. For example, if an io.EOF was encountered while there was +// an unclosed quote or parenthesis. +func IsIncomplete(err error) bool { + perr, ok := err.(ParseError) + return ok && perr.Incomplete +} + // ParseError represents an error found when parsing a source file, from which // the parser cannot recover. type ParseError struct { Filename string Pos Text string + + Incomplete bool } func (e ParseError) Error() string { @@ -691,9 +706,10 @@ func (e LangError) Error() string { func (p *Parser) posErr(pos Pos, format string, a ...interface{}) { p.errPass(ParseError{ - Filename: p.f.Name, - Pos: pos, - Text: fmt.Sprintf(format, a...), + Filename: p.f.Name, + Pos: pos, + Text: fmt.Sprintf(format, a...), + Incomplete: p.tok == _EOF && p.Incomplete(), }) } @@ -978,7 +994,8 @@ func (p *Parser) wordPart() WordPart { p.next() return sq case utf8.RuneSelf: - p.posErr(sq.Pos(), "reached EOF without closing quote %s", sglQuote) + p.tok = _EOF + p.quoteErr(sq.Pos(), sglQuote) return nil } } diff --git a/syntax/parser_test.go b/syntax/parser_test.go index 76ab1f7a7..0086a98f7 100644 --- a/syntax/parser_test.go +++ b/syntax/parser_test.go @@ -2143,3 +2143,39 @@ func TestValidName(t *testing.T) { }) } } + +func TestIsIncomplete(t *testing.T) { + t.Parallel() + + tests := []struct { + in string + want bool + }{ + {"foo\n", false}, + {"foo;", false}, + {"\n", false}, + {"'incomp", true}, + {"foo; 'incomp", true}, + {" (incomp", true}, + {"badsyntax)", false}, + } + p := NewParser() + for i, tc := range tests { + t.Run(fmt.Sprintf("Parse%02d", i), func(t *testing.T) { + r := strings.NewReader(tc.in) + _, err := p.Parse(r, "") + if got := IsIncomplete(err); got != tc.want { + t.Fatalf("%q got %t, wanted %t", tc.in, got, tc.want) + } + }) + t.Run(fmt.Sprintf("Interactive%02d", i), func(t *testing.T) { + r := strings.NewReader(tc.in) + err := p.Interactive(r, func([]*Stmt) bool { + return false + }) + if got := IsIncomplete(err); got != tc.want { + t.Fatalf("%q got %t, wanted %t", tc.in, got, tc.want) + } + }) + } +}