Skip to content

Commit 611a8c3

Browse files
authored
interp: make REPL stricter about parsing errors
So far the REPL loop was treating any parsing error coming from go/parser to generate the AST, as having occurred because the source code was not yet complete (unfinished block). And it was therefore ignoring all of them. However, some of these errors are legitimate, and must be caught as soon as they occur, otherwise the REPL cycle would stay in an errored state forever (even when the block terminates), without the user getting any feedback about it. Therefore, this change adds an extra check when a parsing error occurs, i.e. it verifies that it looks like an "EOF" error (unfinished block) before it ignores it (as the user is supposed to terminate the block eventually). Otherwise the error is treated just like a "non-parsing" (cfg, gta, ...) error and printed out. Fixes #637
1 parent e71ddc7 commit 611a8c3

File tree

2 files changed

+137
-4
lines changed

2 files changed

+137
-4
lines changed

interp/interp.go

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"runtime"
1515
"runtime/debug"
1616
"strconv"
17+
"strings"
1718
"sync"
1819
"sync/atomic"
1920
)
@@ -492,6 +493,15 @@ func (interp *Interpreter) Use(values Exports) {
492493
}
493494
}
494495

496+
type scannerErrors scanner.ErrorList
497+
498+
func (sce scannerErrors) isEOF() bool {
499+
for _, v := range sce {
500+
return strings.HasSuffix(v.Msg, `, found 'EOF'`)
501+
}
502+
return true
503+
}
504+
495505
// REPL performs a Read-Eval-Print-Loop on input reader.
496506
// Results are printed on output writer.
497507
func (interp *Interpreter) REPL(in io.Reader, out io.Writer) {
@@ -527,10 +537,13 @@ func (interp *Interpreter) REPL(in io.Reader, out io.Writer) {
527537
if err != nil {
528538
switch e := err.(type) {
529539
case scanner.ErrorList:
530-
// Early failure in the scanner: the source is incomplete
531-
// and no AST could be produced, neither compiled / run.
532-
// Get one more line, and retry
533-
continue
540+
if scannerErrors(e).isEOF() {
541+
// Early failure in the scanner: the source is incomplete
542+
// and no AST could be produced, neither compiled / run.
543+
// Get one more line, and retry
544+
continue
545+
}
546+
fmt.Fprintln(out, err)
534547
case Panic:
535548
fmt.Fprintln(out, e.Value)
536549
fmt.Fprintln(out, string(e.Stack))
@@ -541,6 +554,7 @@ func (interp *Interpreter) REPL(in io.Reader, out io.Writer) {
541554
src = ""
542555
prompt(v)
543556
}
557+
// TODO(mpl): log s.Err() if not nil?
544558
}
545559

546560
// Repl performs a Read-Eval-Print-Loop on input file descriptor.

interp/interp_eval_test.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
package interp_test
22

33
import (
4+
"bytes"
45
"context"
56
"fmt"
7+
"io"
68
"log"
79
"net/http"
10+
"os"
811
"reflect"
12+
"strconv"
913
"strings"
14+
"sync"
1015
"testing"
1116
"time"
1217

@@ -611,3 +616,117 @@ func assertEval(t *testing.T, i *interp.Interpreter, src, expectedError, expecte
611616
t.Fatalf("got %v, want %s", res, expectedRes)
612617
}
613618
}
619+
620+
func TestEvalEOF(t *testing.T) {
621+
tests := []struct {
622+
desc string
623+
src []string
624+
errorLine int
625+
}{
626+
{
627+
desc: "no error",
628+
src: []string{
629+
`func main() {`,
630+
`println("foo")`,
631+
`}`,
632+
},
633+
errorLine: -1,
634+
},
635+
{
636+
desc: "no parsing error, but block error",
637+
src: []string{
638+
`func main() {`,
639+
`println(foo)`,
640+
`}`,
641+
},
642+
errorLine: 2,
643+
},
644+
{
645+
desc: "parsing error",
646+
src: []string{
647+
`func main() {`,
648+
`println(/foo)`,
649+
`}`,
650+
},
651+
errorLine: 1,
652+
},
653+
}
654+
655+
for it, test := range tests {
656+
i := interp.New(interp.Options{})
657+
var stderr bytes.Buffer
658+
safeStderr := &safeBuffer{buf: &stderr}
659+
pin, pout := io.Pipe()
660+
defer func() {
661+
// Closing the pipe also takes care of making i.REPL terminate,
662+
// hence freeing its goroutine.
663+
_ = pin.Close()
664+
_ = pout.Close()
665+
}()
666+
667+
go func() {
668+
i.REPL(pin, safeStderr)
669+
}()
670+
for k, v := range test.src {
671+
if _, err := pout.Write([]byte(v + "\n")); err != nil {
672+
t.Error(err)
673+
}
674+
Sleep(100 * time.Millisecond)
675+
676+
errMsg := safeStderr.String()
677+
if k == test.errorLine {
678+
if errMsg == "" {
679+
t.Fatalf("%d: statement %q should have produced an error", it, v)
680+
}
681+
break
682+
}
683+
if errMsg != "" {
684+
t.Fatalf("%d: unexpected error: %v", it, errMsg)
685+
}
686+
}
687+
}
688+
}
689+
690+
type safeBuffer struct {
691+
mu sync.RWMutex
692+
buf *bytes.Buffer
693+
}
694+
695+
func (sb *safeBuffer) Read(p []byte) (int, error) {
696+
return sb.buf.Read(p)
697+
}
698+
699+
func (sb *safeBuffer) String() string {
700+
sb.mu.RLock()
701+
defer sb.mu.RUnlock()
702+
return sb.buf.String()
703+
}
704+
705+
func (sb *safeBuffer) Write(p []byte) (int, error) {
706+
sb.mu.Lock()
707+
defer sb.mu.Unlock()
708+
return sb.buf.Write(p)
709+
}
710+
711+
const (
712+
// CITimeoutMultiplier is the multiplier for all timeouts in the CI.
713+
CITimeoutMultiplier = 3
714+
)
715+
716+
// Sleep pauses the current goroutine for at least the duration d.
717+
func Sleep(d time.Duration) {
718+
d = applyCIMultiplier(d)
719+
time.Sleep(d)
720+
}
721+
722+
func applyCIMultiplier(timeout time.Duration) time.Duration {
723+
ci := os.Getenv("CI")
724+
if ci == "" {
725+
return timeout
726+
}
727+
b, err := strconv.ParseBool(ci)
728+
if err != nil || !b {
729+
return timeout
730+
}
731+
return time.Duration(float64(timeout) * CITimeoutMultiplier)
732+
}

0 commit comments

Comments
 (0)