github.com/elek/golangci-lint@v1.42.2-0.20211208090441-c05b7fcb3a9a/test/errchk.go (about)

     1  package test
     2  
     3  import (
     4  	"bytes"
     5  	"errors"
     6  	"fmt"
     7  	"io/ioutil"
     8  	"log"
     9  	"regexp"
    10  	"strconv"
    11  	"strings"
    12  )
    13  
    14  var errorLineRx = regexp.MustCompile(`^\S+?: (.*)\((\S+?)\)$`)
    15  
    16  // errorCheck matches errors in outStr against comments in source files.
    17  // For each line of the source files which should generate an error,
    18  // there should be a comment of the form // ERROR "regexp".
    19  // If outStr has an error for a line which has no such comment,
    20  // this function will report an error.
    21  // Likewise if outStr does not have an error for a line which has a comment,
    22  // or if the error message does not match the <regexp>.
    23  // The <regexp> syntax is Perl but it's best to stick to egrep.
    24  //
    25  // Sources files are supplied as fullshort slice.
    26  // It consists of pairs: full path to source file and its base name.
    27  //nolint:gocyclo,funlen
    28  func errorCheck(outStr string, wantAuto bool, defaultWantedLinter string, fullshort ...string) (err error) {
    29  	var errs []error
    30  	out := splitOutput(outStr, wantAuto)
    31  	// Cut directory name.
    32  	for i := range out {
    33  		for j := 0; j < len(fullshort); j += 2 {
    34  			full, short := fullshort[j], fullshort[j+1]
    35  			out[i] = strings.Replace(out[i], full, short, -1)
    36  		}
    37  	}
    38  
    39  	var want []wantedError
    40  	for j := 0; j < len(fullshort); j += 2 {
    41  		full, short := fullshort[j], fullshort[j+1]
    42  		want = append(want, wantedErrors(full, short, defaultWantedLinter)...)
    43  	}
    44  	for _, we := range want {
    45  		if we.linter == "" {
    46  			err := fmt.Errorf("%s:%d: no expected linter indicated for test",
    47  				we.file, we.lineNum)
    48  			errs = append(errs, err)
    49  			continue
    50  		}
    51  
    52  		var errmsgs []string
    53  		if we.auto {
    54  			errmsgs, out = partitionStrings("<autogenerated>", out)
    55  		} else {
    56  			errmsgs, out = partitionStrings(we.prefix, out)
    57  		}
    58  		if len(errmsgs) == 0 {
    59  			errs = append(errs, fmt.Errorf("%s:%d: missing error %q", we.file, we.lineNum, we.reStr))
    60  			continue
    61  		}
    62  		matched := false
    63  		var textsToMatch []string
    64  		for _, errmsg := range errmsgs {
    65  			// Assume errmsg says "file:line: foo (<linter>)".
    66  			matches := errorLineRx.FindStringSubmatch(errmsg)
    67  			if len(matches) == 0 {
    68  				err := fmt.Errorf("%s:%d: unexpected error line: %s",
    69  					we.file, we.lineNum, errmsg)
    70  				errs = append(errs, err)
    71  				continue
    72  			}
    73  
    74  			text, actualLinter := matches[1], matches[2]
    75  
    76  			if we.re.MatchString(text) {
    77  				matched = true
    78  			} else {
    79  				out = append(out, errmsg)
    80  				textsToMatch = append(textsToMatch, text)
    81  			}
    82  
    83  			if actualLinter != we.linter {
    84  				err := fmt.Errorf("%s:%d: expected error from %q but got error from %q in:\n\t%s",
    85  					we.file, we.lineNum, we.linter, actualLinter, strings.Join(out, "\n\t"))
    86  				errs = append(errs, err)
    87  			}
    88  		}
    89  		if !matched {
    90  			err := fmt.Errorf("%s:%d: no match for %#q vs %q in:\n\t%s",
    91  				we.file, we.lineNum, we.reStr, textsToMatch, strings.Join(out, "\n\t"))
    92  			errs = append(errs, err)
    93  			continue
    94  		}
    95  	}
    96  
    97  	if len(out) > 0 {
    98  		errs = append(errs, fmt.Errorf("unmatched errors"))
    99  		for _, errLine := range out {
   100  			errs = append(errs, fmt.Errorf("%s", errLine))
   101  		}
   102  	}
   103  
   104  	if len(errs) == 0 {
   105  		return nil
   106  	}
   107  	if len(errs) == 1 {
   108  		return errs[0]
   109  	}
   110  	var buf bytes.Buffer
   111  	fmt.Fprintf(&buf, "\n")
   112  	for _, err := range errs {
   113  		fmt.Fprintf(&buf, "%s\n", err.Error())
   114  	}
   115  	return errors.New(buf.String())
   116  }
   117  
   118  func splitOutput(out string, wantAuto bool) []string {
   119  	// gc error messages continue onto additional lines with leading tabs.
   120  	// Split the output at the beginning of each line that doesn't begin with a tab.
   121  	// <autogenerated> lines are impossible to match so those are filtered out.
   122  	var res []string
   123  	for _, line := range strings.Split(out, "\n") {
   124  		line = strings.TrimSuffix(line, "\r") // normalize Windows output
   125  		if strings.HasPrefix(line, "\t") {
   126  			res[len(res)-1] += "\n" + line
   127  		} else if strings.HasPrefix(line, "go tool") || strings.HasPrefix(line, "#") || !wantAuto && strings.HasPrefix(line, "<autogenerated>") {
   128  			continue
   129  		} else if strings.TrimSpace(line) != "" {
   130  			res = append(res, line)
   131  		}
   132  	}
   133  	return res
   134  }
   135  
   136  // matchPrefix reports whether s starts with file name prefix followed by a :,
   137  // and possibly preceded by a directory name.
   138  func matchPrefix(s, prefix string) bool {
   139  	i := strings.Index(s, ":")
   140  	if i < 0 {
   141  		return false
   142  	}
   143  	j := strings.LastIndex(s[:i], "/")
   144  	s = s[j+1:]
   145  	if len(s) <= len(prefix) || s[:len(prefix)] != prefix {
   146  		return false
   147  	}
   148  	if s[len(prefix)] == ':' {
   149  		return true
   150  	}
   151  	return false
   152  }
   153  
   154  func partitionStrings(prefix string, strs []string) (matched, unmatched []string) {
   155  	for _, s := range strs {
   156  		if matchPrefix(s, prefix) {
   157  			matched = append(matched, s)
   158  		} else {
   159  			unmatched = append(unmatched, s)
   160  		}
   161  	}
   162  	return
   163  }
   164  
   165  type wantedError struct {
   166  	reStr   string
   167  	re      *regexp.Regexp
   168  	lineNum int
   169  	auto    bool // match <autogenerated> line
   170  	file    string
   171  	prefix  string
   172  	linter  string
   173  }
   174  
   175  var (
   176  	errRx          = regexp.MustCompile(`// (?:GC_)?ERROR (.*)`)
   177  	errAutoRx      = regexp.MustCompile(`// (?:GC_)?ERRORAUTO (.*)`)
   178  	linterPrefixRx = regexp.MustCompile("^\\s*([^\\s\"`]+)")
   179  )
   180  
   181  // wantedErrors parses expected errors from comments in a file.
   182  //nolint:nakedret
   183  func wantedErrors(file, short, defaultLinter string) (errs []wantedError) {
   184  	cache := make(map[string]*regexp.Regexp)
   185  
   186  	src, err := ioutil.ReadFile(file)
   187  	if err != nil {
   188  		log.Fatal(err)
   189  	}
   190  	for i, line := range strings.Split(string(src), "\n") {
   191  		lineNum := i + 1
   192  		if strings.Contains(line, "////") {
   193  			// double comment disables ERROR
   194  			continue
   195  		}
   196  		var auto bool
   197  		m := errAutoRx.FindStringSubmatch(line)
   198  		if m != nil {
   199  			auto = true
   200  		} else {
   201  			m = errRx.FindStringSubmatch(line)
   202  		}
   203  		if m == nil {
   204  			continue
   205  		}
   206  		rest := m[1]
   207  		linter := defaultLinter
   208  		if lm := linterPrefixRx.FindStringSubmatch(rest); lm != nil {
   209  			linter = lm[1]
   210  			rest = rest[len(lm[0]):]
   211  		}
   212  		rx, err := strconv.Unquote(strings.TrimSpace(rest))
   213  		if err != nil {
   214  			log.Fatalf("%s:%d: invalid errchk line: %s, %v", file, lineNum, line, err)
   215  		}
   216  		re := cache[rx]
   217  		if re == nil {
   218  			var err error
   219  			re, err = regexp.Compile(rx)
   220  			if err != nil {
   221  				log.Fatalf("%s:%d: invalid regexp \"%#q\" in ERROR line: %v", file, lineNum, rx, err)
   222  			}
   223  			cache[rx] = re
   224  		}
   225  		prefix := fmt.Sprintf("%s:%d", short, lineNum)
   226  		errs = append(errs, wantedError{
   227  			reStr:   rx,
   228  			re:      re,
   229  			prefix:  prefix,
   230  			auto:    auto,
   231  			lineNum: lineNum,
   232  			file:    short,
   233  			linter:  linter,
   234  		})
   235  	}
   236  
   237  	return
   238  }