github.com/khulnasoft/codebase@v0.0.0-20231214144635-a707781cbb24/errorformat/errorformat.go (about) 1 // Package errorformat provides 'errorformat' functionality of Vim. :h 2 // errorformat 3 package errorformat 4 5 import ( 6 "bufio" 7 "bytes" 8 "errors" 9 "fmt" 10 "io" 11 "regexp" 12 "strconv" 13 "strings" 14 ) 15 16 // Errorformat provides errorformat feature. 17 type Errorformat struct { 18 Efms []*Efm 19 } 20 21 // Scanner provides a interface for scanning compiler/linter/static analyzer 22 // result using Errorformat. 23 type Scanner struct { 24 *Errorformat 25 source *bufio.Scanner 26 27 qi *qfinfo 28 29 entry *Entry // entry which is returned by Entry() func 30 mlpoped bool // is multiline entry poped (for non-end multiline entry) 31 } 32 33 // NewErrorformat compiles given errorformats string (efms) and returns a new 34 // Errorformat. It returns error if the errorformat is invalid. 35 func NewErrorformat(efms []string) (*Errorformat, error) { 36 errorformat := &Errorformat{Efms: make([]*Efm, 0, len(efms))} 37 for _, efm := range efms { 38 e, err := NewEfm(efm) 39 if err != nil { 40 return nil, err 41 } 42 errorformat.Efms = append(errorformat.Efms, e) 43 } 44 return errorformat, nil 45 } 46 47 // NewScanner returns a new Scanner to read from r. 48 func (errorformat *Errorformat) NewScanner(r io.Reader) *Scanner { 49 return &Scanner{ 50 Errorformat: errorformat, 51 source: bufio.NewScanner(r), 52 qi: &qfinfo{}, 53 mlpoped: true, 54 } 55 } 56 57 type qfinfo struct { 58 filestack []string 59 currfile string 60 dirstack []string 61 directory string 62 multiscan bool 63 multiline bool 64 multiignore bool 65 66 qflist []*Entry 67 } 68 69 type qffields struct { 70 namebuf string 71 errmsg string 72 lnum int 73 col int 74 endlnum int 75 endcol int 76 useviscol bool 77 pattern string 78 enr int 79 etype byte 80 valid bool 81 82 lines []string 83 } 84 85 // Entry represents matched entry of errorformat, equivalent to Vim's quickfix 86 // list item. 87 type Entry struct { 88 // name of a file 89 Filename string `json:"filename"` 90 // line number 91 Lnum int `json:"lnum"` 92 // End of line number if the item is multiline 93 EndLnum int `json:"end_lnum"` 94 // column number (first column is 1) 95 Col int `json:"col"` 96 // End of column number if the item has range 97 EndCol int `json:"end_col"` 98 // true: "col" is visual column 99 // false: "col" is byte index 100 Vcol bool `json:"vcol"` 101 // error number 102 Nr int `json:"nr"` 103 // search pattern used to locate the error 104 Pattern string `json:"pattern"` 105 // description of the error 106 Text string `json:"text"` 107 // type of the error, 'E', '1', etc. 108 Type rune `json:"type"` 109 // true: recognized error message 110 Valid bool `json:"valid"` 111 112 // Original error lines (often one line. more than one line for multi-line 113 // errorformat. :h errorformat-multi-line) 114 Lines []string `json:"lines"` 115 } 116 117 // || message 118 // /path/to/file|| message 119 // /path/to/file|1| message 120 // /path/to/file|1 col 14| message 121 // /path/to/file|1 col 14 error 8| message 122 // /path/to/file|1-2 col 14-28| message 123 // {filename}|{lnum}[-{end_lnum}][ col {col}[-{end_col}]][ {type} [{nr}]]| {text} 124 func (e *Entry) String() string { 125 var b strings.Builder 126 b.WriteString(e.Filename) 127 b.WriteRune('|') 128 if e.Lnum > 0 { 129 b.WriteString(strconv.Itoa(e.Lnum)) 130 if e.EndLnum > 0 { 131 b.WriteString(fmt.Sprintf("-%d", e.EndLnum)) 132 } 133 } 134 if e.Col > 0 { 135 b.WriteString(fmt.Sprintf(" col %d", e.Col)) 136 if e.EndCol > 0 { 137 b.WriteString(fmt.Sprintf("-%d", e.EndCol)) 138 } 139 } 140 if t := e.Types(); t != "" { 141 b.WriteRune(' ') 142 b.WriteString(t) 143 } 144 b.WriteRune('|') 145 if e.Text != "" { 146 b.WriteRune(' ') 147 b.WriteString(e.Text) 148 } 149 return b.String() 150 } 151 152 // Types makes a nice message out of the error character and the error number: 153 // 154 // qf_types in src/quickfix.c 155 func (e *Entry) Types() string { 156 s := "" 157 switch e.Type { 158 case 'e', 'E': 159 s = "error" 160 case 0: 161 if e.Nr > 0 { 162 s = "error" 163 } 164 case 'w', 'W': 165 s = "warning" 166 case 'i', 'I': 167 s = "info" 168 case 'n', 'N': 169 s = "note" 170 default: 171 s = string(e.Type) 172 } 173 if e.Nr > 0 { 174 if s != "" { 175 s += " " 176 } 177 s += strconv.Itoa(e.Nr) 178 } 179 return s 180 } 181 182 // Scan advances the Scanner to the next entry matched with errorformat, which 183 // will then be available through the Entry method. It returns false 184 // when the scan stops by reaching the end of the input. 185 func (s *Scanner) Scan() bool { 186 for s.source.Scan() { 187 line := s.source.Text() 188 status, fields := s.parseLine(line) 189 switch status { 190 case qffail: 191 continue 192 case qfendmultiline: 193 s.mlpoped = true 194 s.entry = s.qi.qflist[len(s.qi.qflist)-1] 195 return true 196 case qfignoreline: 197 continue 198 } 199 var lastml *Entry // last multiline entry which isn't poped out 200 if !s.mlpoped { 201 lastml = s.qi.qflist[len(s.qi.qflist)-1] 202 } 203 qfl := &Entry{ 204 Filename: fields.namebuf, 205 Lnum: fields.lnum, 206 EndLnum: fields.endlnum, 207 Col: fields.col, 208 EndCol: fields.endcol, 209 Nr: fields.enr, 210 Pattern: fields.pattern, 211 Text: fields.errmsg, 212 Vcol: fields.useviscol, 213 Valid: fields.valid, 214 Type: rune(fields.etype), 215 Lines: fields.lines, 216 } 217 if qfl.Filename == "" && s.qi.currfile != "" { 218 qfl.Filename = s.qi.currfile 219 } 220 s.qi.qflist = append(s.qi.qflist, qfl) 221 if s.qi.multiline { 222 s.mlpoped = false // mark multiline entry is not poped 223 // if there is last multiline entry which isn't poped out yet, pop it out now. 224 if lastml != nil { 225 s.entry = lastml 226 return true 227 } 228 continue 229 } 230 // multiline flag doesn't be reset with new entry. 231 // %Z or nomach are the only way to reset multiline flag. 232 s.entry = qfl 233 return true 234 } 235 // pop last not-ended multiline entry 236 if !s.mlpoped { 237 s.mlpoped = true 238 s.entry = s.qi.qflist[len(s.qi.qflist)-1] 239 return true 240 } 241 return false 242 } 243 244 // Entry returns the most recent entry generated by a call to Scan. 245 func (s *Scanner) Entry() *Entry { 246 return s.entry 247 } 248 249 type qfstatus int 250 251 const ( 252 qffail qfstatus = iota 253 qfignoreline 254 qfendmultiline 255 qfok 256 ) 257 258 func (s *Scanner) parseLine(line string) (qfstatus, *qffields) { 259 return s.parseLineInternal(line, 0) 260 } 261 262 func (s *Scanner) parseLineInternal(line string, i int) (qfstatus, *qffields) { 263 fields := &qffields{valid: true, enr: -1, lines: []string{line}} 264 tail := "" 265 var idx byte 266 nomatch := false 267 var efm *Efm 268 for ; i <= len(s.Efms); i++ { 269 if i == len(s.Efms) { 270 nomatch = true 271 break 272 } 273 efm = s.Efms[i] 274 275 idx = efm.prefix 276 if s.qi.multiscan && strchar("OPQ", idx) { 277 continue 278 } 279 280 if (idx == 'C' || idx == 'Z') && !s.qi.multiline { 281 continue 282 } 283 284 r := efm.Match(line) 285 if r == nil { 286 continue 287 } 288 289 if strchar("EWI", idx) { 290 fields.etype = idx 291 } 292 293 if r.F != "" { // %f 294 fields.namebuf = r.F 295 if strchar("OPQ", idx) && !fileexists(fields.namebuf) { 296 continue 297 } 298 } 299 fields.enr = r.N // %n 300 fields.lnum = r.L // %l 301 fields.endlnum = r.E // %e 302 fields.col = r.C // %c 303 fields.endcol = r.K // %k 304 if r.T != 0 { 305 fields.etype = r.T // %t 306 } 307 if efm.flagplus && !s.qi.multiscan { // %+ 308 fields.errmsg = line 309 } else if r.M != "" { 310 fields.errmsg = r.M 311 } 312 tail = r.R // %r 313 if r.P != "" { // %p 314 fields.useviscol = true 315 fields.col = 0 316 for _, m := range r.P { 317 fields.col++ 318 if m == '\t' { 319 fields.col += 7 320 fields.col -= fields.col % 8 321 } 322 } 323 fields.col++ // last pointer (e.g. ^) 324 } 325 if r.V != 0 { 326 fields.useviscol = true 327 fields.col = r.V 328 } 329 if r.S != "" { 330 fields.pattern = fmt.Sprintf("^%v$", regexp.QuoteMeta(r.S)) 331 } 332 break 333 } 334 s.qi.multiscan = false 335 if nomatch || idx == 'D' || idx == 'X' { 336 if !nomatch { 337 if idx == 'D' { 338 if fields.namebuf == "" { 339 return qffail, nil 340 } 341 s.qi.directory = fields.namebuf 342 s.qi.dirstack = append(s.qi.dirstack, s.qi.directory) 343 } else if idx == 'X' && len(s.qi.dirstack) > 0 { 344 s.qi.directory = s.qi.dirstack[len(s.qi.dirstack)-1] 345 s.qi.dirstack = s.qi.dirstack[:len(s.qi.dirstack)-1] 346 } 347 } 348 fields.namebuf = "" 349 fields.lnum = 0 350 fields.valid = false 351 fields.errmsg = line 352 if nomatch { 353 s.qi.multiline = false 354 s.qi.multiignore = false 355 } 356 } else if !nomatch { 357 if strchar("AEWI", idx) { 358 s.qi.multiline = true // start of a multi-line message 359 s.qi.multiignore = false // reset continuation 360 } else if strchar("CZ", idx) { 361 // continuation of multi-line msg 362 if !s.qi.multiignore { 363 qfprev := s.qi.qflist[len(s.qi.qflist)-1] 364 if qfprev == nil { 365 return qffail, nil 366 } 367 qfprev.Lines = append(qfprev.Lines, line) 368 if fields.errmsg != "" && !s.qi.multiignore { 369 if qfprev.Text == "" { 370 qfprev.Text = fields.errmsg 371 } else { 372 qfprev.Text += "\n" + fields.errmsg 373 } 374 } 375 if qfprev.Nr < 1 { 376 qfprev.Nr = fields.enr 377 } 378 if fields.etype != 0 && qfprev.Type == 0 { 379 qfprev.Type = rune(fields.etype) 380 } 381 if qfprev.Filename == "" { 382 qfprev.Filename = fields.namebuf 383 } 384 if qfprev.Lnum == 0 { 385 qfprev.Lnum = fields.lnum 386 } 387 if qfprev.EndLnum == 0 { 388 qfprev.EndLnum = fields.endlnum 389 } 390 if qfprev.Col == 0 { 391 qfprev.Col = fields.col 392 } 393 if qfprev.EndCol == 0 { 394 qfprev.EndCol = fields.endcol 395 } 396 qfprev.Vcol = fields.useviscol 397 } 398 if idx == 'Z' { 399 s.qi.multiline = false 400 s.qi.multiignore = false 401 return qfendmultiline, fields 402 } 403 return qfignoreline, nil 404 } else if strchar("OPQ", idx) { 405 // global file names 406 fields.valid = false 407 if fields.namebuf == "" || fileexists(fields.namebuf) { 408 if fields.namebuf != "" && idx == 'P' { 409 s.qi.currfile = fields.namebuf 410 s.qi.filestack = append(s.qi.filestack, s.qi.currfile) 411 } else if idx == 'Q' && len(s.qi.filestack) > 0 { 412 s.qi.currfile = s.qi.filestack[len(s.qi.filestack)-1] 413 s.qi.filestack = s.qi.filestack[:len(s.qi.filestack)-1] 414 } 415 fields.namebuf = "" 416 if tail != "" { 417 s.qi.multiscan = true 418 return s.parseLineInternal(strings.TrimLeft(tail, " \t"), i) 419 } 420 } 421 } 422 if efm.flagminus { // generally exclude this line 423 if s.qi.multiline { // also exclude continuation lines 424 s.qi.multiignore = true 425 } 426 return qfignoreline, nil 427 } 428 } 429 return qfok, fields 430 } 431 432 // Efm represents a errorformat. 433 type Efm struct { 434 regex *regexp.Regexp 435 436 flagplus bool 437 flagminus bool 438 prefix byte 439 } 440 441 var fmtpattern = map[byte]string{ 442 'f': `(?P<f>(?:[[:alpha:]]:)?(?:\\ |[^ ])+?)`, 443 'n': `(?P<n>\d+)`, 444 'l': `(?P<l>\d+)`, 445 'e': `(?P<e>\d+)`, 446 'c': `(?P<c>\d+)`, 447 'k': `(?P<k>\d+)`, 448 't': `(?P<t>.)`, 449 'm': `(?P<m>.+)`, 450 'r': `(?P<r>.*)`, 451 'p': `(?P<p>[- .]*)`, 452 'v': `(?P<v>\d+)`, 453 's': `(?P<s>.+)`, 454 } 455 456 // NewEfm converts a 'errorformat' string to regular expression pattern with 457 // flags and returns Efm. 458 // 459 // quickfix.c: efm_to_regpat 460 func NewEfm(errorformat string) (*Efm, error) { 461 var regpat bytes.Buffer 462 var efmp byte 463 var i = 0 464 var incefmp = func() { 465 i++ 466 efmp = errorformat[i] 467 } 468 efm := &Efm{} 469 regpat.WriteRune('^') 470 for ; i < len(errorformat); i++ { 471 efmp = errorformat[i] 472 if efmp == '%' { 473 incefmp() 474 // - do not support %> 475 if re, ok := fmtpattern[efmp]; ok { 476 regpat.WriteString(re) 477 } else if efmp == '*' { 478 incefmp() 479 if efmp == '[' || efmp == '\\' { 480 regpat.WriteByte(efmp) 481 if efmp == '[' { // %*[^a-z0-9] etc. 482 incefmp() 483 for efmp != ']' { 484 regpat.WriteByte(efmp) 485 if i == len(errorformat)-1 { 486 return nil, errors.New("E374: Missing ] in format string") 487 } 488 incefmp() 489 } 490 regpat.WriteByte(efmp) 491 } else { // %*\D, %*\s etc. 492 incefmp() 493 regpat.WriteByte(efmp) 494 } 495 regpat.WriteRune('+') 496 } else { 497 return nil, fmt.Errorf("E375: Unsupported %%%v in format string", string(efmp)) 498 } 499 } else if (efmp == '+' || efmp == '-') && 500 i < len(errorformat)-1 && 501 strchar("DXAEWICZGOPQ", errorformat[i+1]) { 502 if efmp == '+' { 503 efm.flagplus = true 504 incefmp() 505 } else if efmp == '-' { 506 efm.flagminus = true 507 incefmp() 508 } 509 efm.prefix = efmp 510 } else if strchar(`%\.^$?+[`, efmp) { 511 // regexp magic characters 512 regpat.WriteByte(efmp) 513 } else if efmp == '#' { 514 regpat.WriteRune('*') 515 } else { 516 if strchar("DXAEWICZGOPQ", efmp) { 517 efm.prefix = efmp 518 } else { 519 return nil, fmt.Errorf("E376: Invalid %%%v in format string prefix", string(efmp)) 520 } 521 } 522 } else { // copy normal character 523 if efmp == '\\' && i < len(errorformat)-1 { 524 incefmp() 525 } else if strchar(`.+*()|[{^$`, efmp) { // escape regexp atoms 526 regpat.WriteRune('\\') 527 } 528 regpat.WriteByte(efmp) 529 } 530 } 531 regpat.WriteRune('$') 532 re, err := regexp.Compile(regpat.String()) 533 if err != nil { 534 return nil, err 535 } 536 efm.regex = re 537 return efm, nil 538 } 539 540 // Match represents match of Efm. ref: Basic items in :h errorformat 541 type Match struct { 542 F string // (%f) file name 543 N int // (%n) error number 544 L int // (%l) line number 545 C int // (%c) column number 546 T byte // (%t) error type 547 M string // (%m) error message 548 R string // (%r) the "rest" of a single-line file message 549 P string // (%p) pointer line 550 V int // (%v) virtual column number 551 S string // (%s) search text 552 553 // Extensions 554 E int // (%e) end line number 555 K int // (%k) end column number 556 } 557 558 // Match returns match against given string. 559 func (efm *Efm) Match(s string) *Match { 560 ms := efm.regex.FindStringSubmatch(s) 561 if len(ms) == 0 { 562 return nil 563 } 564 match := &Match{} 565 names := efm.regex.SubexpNames() 566 for i, name := range names { 567 if i == 0 { 568 continue 569 } 570 m := ms[i] 571 switch name { 572 case "f": 573 match.F = m 574 case "n": 575 match.N = mustAtoI(m) 576 case "l": 577 match.L = mustAtoI(m) 578 case "e": 579 match.E = mustAtoI(m) 580 case "c": 581 match.C = mustAtoI(m) 582 case "k": 583 match.K = mustAtoI(m) 584 case "t": 585 match.T = m[0] 586 case "m": 587 match.M = m 588 case "r": 589 match.R = m 590 case "p": 591 match.P = m 592 case "v": 593 match.V = mustAtoI(m) 594 case "s": 595 match.S = m 596 } 597 } 598 return match 599 } 600 601 func strchar(chars string, c byte) bool { 602 return bytes.ContainsAny([]byte{c}, chars) 603 } 604 605 func mustAtoI(s string) int { 606 i, _ := strconv.Atoi(s) 607 return i 608 } 609 610 // Vim sees the file exists or not (maybe for quickfix usage), but do not see 611 // file exists this implementation. Always return true. 612 var fileexists = func(filename string) bool { 613 return true 614 // _, err := os.Stat(filename) 615 // return err == nil 616 }