--- /dev/null
+// Copyright 2018 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// This file implements a regression test harness for syntax errors.
+// The files in the testdata directory are parsed and the reported
+// errors are compared against the errors declared in those files.
+//
+// Errors are declared in place in the form of "error comments",
+// just before (or on the same line as) the offending token.
+//
+// Error comments must be of the form // ERROR rx or /* ERROR rx */
+// where rx is a regular expression that matches the reported error
+// message. The rx text comprises the comment text after "ERROR ",
+// with any white space around it stripped.
+//
+// If the line comment form is used, the reported error's line must
+// match the line of the error comment.
+//
+// If the regular comment form is used, the reported error's position
+// must match the position of the token immediately following the
+// error comment. Thus, /* ERROR ... */ comments should appear
+// immediately before the position where the error is reported.
+//
+// Currently, the test harness only supports one error comment per
+// token. If multiple error comments appear before a token, only
+// the last one is considered.
+
+package syntax
+
+import (
+ "flag"
+ "fmt"
+ "internal/testenv"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "regexp"
+ "sort"
+ "strings"
+ "testing"
+)
+
+const testdata = "testdata" // directory containing test files
+
+var print = flag.Bool("print", false, "only print errors")
+
+// A position represents a source position in the current file.
+type position struct {
+ line, col uint
+}
+
+func (pos position) String() string {
+ return fmt.Sprintf("%d:%d", pos.line, pos.col)
+}
+
+func sortedPositions(m map[position]string) []position {
+ list := make([]position, len(m))
+ i := 0
+ for pos := range m {
+ list[i] = pos
+ i++
+ }
+ sort.Slice(list, func(i, j int) bool {
+ a, b := list[i], list[j]
+ return a.line < b.line || a.line == b.line && a.col < b.col
+ })
+ return list
+}
+
+// declaredErrors returns a map of source positions to error
+// patterns, extracted from error comments in the given file.
+// Error comments in the form of line comments use col = 0
+// in their position.
+func declaredErrors(t *testing.T, filename string) map[position]string {
+ f, err := os.Open(filename)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer f.Close()
+
+ declared := make(map[position]string)
+
+ var s scanner
+ var pattern string
+ s.init(f, func(line, col uint, msg string) {
+ // errors never start with '/' so they are automatically excluded here
+ switch {
+ case strings.HasPrefix(msg, "// ERROR "):
+ // we can't have another comment on the same line - just add it
+ declared[position{s.line, 0}] = strings.TrimSpace(msg[9:])
+ case strings.HasPrefix(msg, "/* ERROR "):
+ // we may have more comments before the next token - collect them
+ pattern = strings.TrimSpace(msg[9 : len(msg)-2])
+ }
+ }, comments)
+
+ // consume file
+ for {
+ s.next()
+ if pattern != "" {
+ declared[position{s.line, s.col}] = pattern
+ pattern = ""
+ }
+ if s.tok == _EOF {
+ break
+ }
+ }
+
+ return declared
+}
+
+func testSyntaxErrors(t *testing.T, filename string) {
+ declared := declaredErrors(t, filename)
+ if *print {
+ fmt.Println("Declared errors:")
+ for _, pos := range sortedPositions(declared) {
+ fmt.Printf("%s:%s: %s\n", filename, pos, declared[pos])
+ }
+
+ fmt.Println()
+ fmt.Println("Reported errors:")
+ }
+
+ f, err := os.Open(filename)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer f.Close()
+
+ ParseFile(filename, func(err error) {
+ e, ok := err.(Error)
+ if !ok {
+ return
+ }
+
+ if *print {
+ fmt.Println(err)
+ return
+ }
+
+ orig := position{e.Pos.Line(), e.Pos.Col()}
+ pos := orig
+ pattern, found := declared[pos]
+ if !found {
+ // try line comment (only line must match)
+ pos = position{e.Pos.Line(), 0}
+ pattern, found = declared[pos]
+ }
+ if found {
+ rx, err := regexp.Compile(pattern)
+ if err != nil {
+ t.Errorf("%s: %v", pos, err)
+ return
+ }
+ if match := rx.MatchString(e.Msg); !match {
+ t.Errorf("%s: %q does not match %q", pos, e.Msg, pattern)
+ return
+ }
+ // we have a match - eliminate this error
+ delete(declared, pos)
+ } else {
+ t.Errorf("%s: unexpected error: %s", orig, e.Msg)
+ }
+ }, nil, 0)
+
+ if *print {
+ fmt.Println()
+ return // we're done
+ }
+
+ // report expected but not reported errors
+ for pos, pattern := range declared {
+ t.Errorf("%s: missing error: %s", pos, pattern)
+ }
+}
+
+func TestSyntaxErrors(t *testing.T) {
+ testenv.MustHaveGoBuild(t) // we need access to source (testdata)
+
+ list, err := ioutil.ReadDir(testdata)
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, fi := range list {
+ name := fi.Name()
+ if !fi.IsDir() && !strings.HasPrefix(name, ".") {
+ testSyntaxErrors(t, filepath.Join(testdata, name))
+ }
+ }
+}