// 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))
|
}
|
}
|
}
|