Skip to content

Commit

Permalink
syntax: make IsIncomplete more robust
Browse files Browse the repository at this point in the history
It now works with Parser.Words reliably. It used to rely on counting
the number of open statements, and there are none there.
Moreover, the way it used the current quote state to check
for missing closing tokens was flawed, as it did not work for
closing double quotes or curly braces for parameter expansions.

Fixes #937.
  • Loading branch information
mvdan committed Feb 23, 2025
1 parent 623352b commit cbc2c7b
Show file tree
Hide file tree
Showing 2 changed files with 41 additions and 22 deletions.
26 changes: 13 additions & 13 deletions syntax/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -415,10 +415,10 @@ type Parser struct {

parsingDoc bool // true if using Parser.Document

// openStmts is how many entire statements we're currently parsing. A
// non-zero number means that we require certain tokens or words before
// reaching EOF.
openStmts int
// openNodes tracks how many entire statements or words we're currently parsing.
// A non-zero number means that we require certain tokens or words before
// reaching EOF, used for [Parser.Incomplete].
openNodes int
// openBquotes is how many levels of backquotes are open at the moment.
openBquotes int

Expand All @@ -439,17 +439,15 @@ 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.
// Incomplete reports whether the parser needs more input bytes
// to finish properly parsing a statement or word.
//
// It is only safe to call while the parser is blocked on a read. For an example
// use case, see [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.
// If there are any open statements, we need to finish them.
// If there are any open nodes, we need to finish them.
// If we're constructing a literal, we need to finish it.
return p.quote != noState || p.openStmts > 0 || p.litBs != nil
return p.openNodes > 0 || len(p.litBs) > 0
}

const bufSize = 1 << 10
Expand All @@ -462,7 +460,7 @@ func (p *Parser) reset() {
p.r, p.w = 0, 0
p.err, p.readErr = nil, nil
p.quote, p.forbidNested = noState, false
p.openStmts = 0
p.openNodes = 0
p.recoveredErrors = 0
p.heredocs, p.buriedHdocs = p.heredocs[:0], 0
p.hdocStops = nil
Expand Down Expand Up @@ -943,9 +941,9 @@ loop:
if p.tok == _EOF {
break
}
p.openStmts++
p.openNodes++
s := p.getStmt(true, false, false)
p.openStmts--
p.openNodes--
if s == nil {
p.invalidStmtStart()
break
Expand Down Expand Up @@ -1021,7 +1019,9 @@ func (p *Parser) getLit() *Lit {

func (p *Parser) wordParts(wps []WordPart) []WordPart {
for {
p.openNodes++
n := p.wordPart()
p.openNodes--
if n == nil {
if len(wps) == 0 {
return nil // normalize empty lists into nil
Expand Down
37 changes: 28 additions & 9 deletions syntax/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2470,16 +2470,21 @@ func TestIsIncomplete(t *testing.T) {
t.Parallel()

tests := []struct {
in string
want bool
in string
notWords bool
want bool
}{
{"foo\n", false},
{"foo;", false},
{"\n", false},
{"'incomp", true},
{"foo; 'incomp", true},
{" (incomp", true},
{"badsyntax)", false},
{in: "foo\n", want: false},
{in: "foo;", want: false},
{in: "\n", want: false},
{in: "badsyntax)", want: false},
{in: "foo 'incomp", want: true},
{in: `foo "incomp`, want: true},
{in: "foo ${incomp", want: true},

{in: "foo; 'incomp", notWords: true, want: true},
{in: `foo; "incomp`, notWords: true, want: true},
{in: " (incomp", notWords: true, want: true},
}
p := NewParser()
for i, tc := range tests {
Expand All @@ -2499,6 +2504,20 @@ func TestIsIncomplete(t *testing.T) {
t.Fatalf("%q got %t, wanted %t", tc.in, got, tc.want)
}
})
if !tc.notWords {
t.Run(fmt.Sprintf("WordsSeq%02d", i), func(t *testing.T) {
r := strings.NewReader(tc.in)
var firstErr error
for _, err := range p.WordsSeq(r) {
if err != nil {
firstErr = err
}
}
if got := IsIncomplete(firstErr); got != tc.want {
t.Fatalf("%q got %t, wanted %t", tc.in, got, tc.want)
}
})
}
}
}

Expand Down

0 comments on commit cbc2c7b

Please sign in to comment.