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 }