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 }