github.com/yuqengo/golangci-lint@v0.0.2/test/errchk.go (about)

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