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 }