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  }