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