Source file src/cmd/compile/internal/syntax/error_test.go

     1  // Copyright 2018 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // This file implements a regression test harness for syntax errors.
     6  // The files in the testdata directory are parsed and the reported
     7  // errors are compared against the errors declared in those files.
     8  //
     9  // Errors are declared in place in the form of "error comments",
    10  // just before (or on the same line as) the offending token.
    11  //
    12  // Error comments must be of the form // ERROR rx or /* ERROR rx */
    13  // where rx is a regular expression that matches the reported error
    14  // message. The rx text comprises the comment text after "ERROR ",
    15  // with any white space around it stripped.
    16  //
    17  // If the line comment form is used, the reported error's line must
    18  // match the line of the error comment.
    19  //
    20  // If the regular comment form is used, the reported error's position
    21  // must match the position of the token immediately following the
    22  // error comment. Thus, /* ERROR ... */ comments should appear
    23  // immediately before the position where the error is reported.
    24  //
    25  // Currently, the test harness only supports one error comment per
    26  // token. If multiple error comments appear before a token, only
    27  // the last one is considered.
    28  
    29  package syntax
    30  
    31  import (
    32  	"flag"
    33  	"fmt"
    34  	"internal/testenv"
    35  	"os"
    36  	"path/filepath"
    37  	"regexp"
    38  	"sort"
    39  	"strings"
    40  	"testing"
    41  )
    42  
    43  const testdata = "testdata" // directory containing test files
    44  
    45  var print = flag.Bool("print", false, "only print errors")
    46  
    47  // A position represents a source position in the current file.
    48  type position struct {
    49  	line, col uint
    50  }
    51  
    52  func (pos position) String() string {
    53  	return fmt.Sprintf("%d:%d", pos.line, pos.col)
    54  }
    55  
    56  func sortedPositions(m map[position]string) []position {
    57  	list := make([]position, len(m))
    58  	i := 0
    59  	for pos := range m {
    60  		list[i] = pos
    61  		i++
    62  	}
    63  	sort.Slice(list, func(i, j int) bool {
    64  		a, b := list[i], list[j]
    65  		return a.line < b.line || a.line == b.line && a.col < b.col
    66  	})
    67  	return list
    68  }
    69  
    70  // declaredErrors returns a map of source positions to error
    71  // patterns, extracted from error comments in the given file.
    72  // Error comments in the form of line comments use col = 0
    73  // in their position.
    74  func declaredErrors(t *testing.T, filename string) map[position]string {
    75  	f, err := os.Open(filename)
    76  	if err != nil {
    77  		t.Fatal(err)
    78  	}
    79  	defer f.Close()
    80  
    81  	declared := make(map[position]string)
    82  
    83  	var s scanner
    84  	var pattern string
    85  	s.init(f, func(line, col uint, msg string) {
    86  		// errors never start with '/' so they are automatically excluded here
    87  		switch {
    88  		case strings.HasPrefix(msg, "// ERROR "):
    89  			// we can't have another comment on the same line - just add it
    90  			declared[position{s.line, 0}] = strings.TrimSpace(msg[9:])
    91  		case strings.HasPrefix(msg, "/* ERROR "):
    92  			// we may have more comments before the next token - collect them
    93  			pattern = strings.TrimSpace(msg[9 : len(msg)-2])
    94  		}
    95  	}, comments)
    96  
    97  	// consume file
    98  	for {
    99  		s.next()
   100  		if pattern != "" {
   101  			declared[position{s.line, s.col}] = pattern
   102  			pattern = ""
   103  		}
   104  		if s.tok == _EOF {
   105  			break
   106  		}
   107  	}
   108  
   109  	return declared
   110  }
   111  
   112  func testSyntaxErrors(t *testing.T, filename string) {
   113  	declared := declaredErrors(t, filename)
   114  	if *print {
   115  		fmt.Println("Declared errors:")
   116  		for _, pos := range sortedPositions(declared) {
   117  			fmt.Printf("%s:%s: %s\n", filename, pos, declared[pos])
   118  		}
   119  
   120  		fmt.Println()
   121  		fmt.Println("Reported errors:")
   122  	}
   123  
   124  	f, err := os.Open(filename)
   125  	if err != nil {
   126  		t.Fatal(err)
   127  	}
   128  	defer f.Close()
   129  
   130  	ParseFile(filename, func(err error) {
   131  		e, ok := err.(Error)
   132  		if !ok {
   133  			return
   134  		}
   135  
   136  		if *print {
   137  			fmt.Println(err)
   138  			return
   139  		}
   140  
   141  		orig := position{e.Pos.Line(), e.Pos.Col()}
   142  		pos := orig
   143  		pattern, found := declared[pos]
   144  		if !found {
   145  			// try line comment (only line must match)
   146  			pos = position{e.Pos.Line(), 0}
   147  			pattern, found = declared[pos]
   148  		}
   149  		if found {
   150  			rx, err := regexp.Compile(pattern)
   151  			if err != nil {
   152  				t.Errorf("%s:%s: %v", filename, pos, err)
   153  				return
   154  			}
   155  			if match := rx.MatchString(e.Msg); !match {
   156  				t.Errorf("%s:%s: %q does not match %q", filename, pos, e.Msg, pattern)
   157  				return
   158  			}
   159  			// we have a match - eliminate this error
   160  			delete(declared, pos)
   161  		} else {
   162  			t.Errorf("%s:%s: unexpected error: %s", filename, orig, e.Msg)
   163  		}
   164  	}, nil, CheckBranches)
   165  
   166  	if *print {
   167  		fmt.Println()
   168  		return // we're done
   169  	}
   170  
   171  	// report expected but not reported errors
   172  	for pos, pattern := range declared {
   173  		t.Errorf("%s:%s: missing error: %s", filename, pos, pattern)
   174  	}
   175  }
   176  
   177  func TestSyntaxErrors(t *testing.T) {
   178  	testenv.MustHaveGoBuild(t) // we need access to source (testdata)
   179  
   180  	list, err := os.ReadDir(testdata)
   181  	if err != nil {
   182  		t.Fatal(err)
   183  	}
   184  	for _, fi := range list {
   185  		name := fi.Name()
   186  		if !fi.IsDir() && !strings.HasPrefix(name, ".") {
   187  			testSyntaxErrors(t, filepath.Join(testdata, name))
   188  		}
   189  	}
   190  }
   191  

View as plain text