Home | History | Annotate | Download | only in token
      1 // Copyright 2010 The Go Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style
      3 // license that can be found in the LICENSE file.
      4 
      5 package token
      6 
      7 import (
      8 	"fmt"
      9 	"sort"
     10 	"sync"
     11 )
     12 
     13 // -----------------------------------------------------------------------------
     14 // Positions
     15 
     16 // Position describes an arbitrary source position
     17 // including the file, line, and column location.
     18 // A Position is valid if the line number is > 0.
     19 //
     20 type Position struct {
     21 	Filename string // filename, if any
     22 	Offset   int    // offset, starting at 0
     23 	Line     int    // line number, starting at 1
     24 	Column   int    // column number, starting at 1 (byte count)
     25 }
     26 
     27 // IsValid reports whether the position is valid.
     28 func (pos *Position) IsValid() bool { return pos.Line > 0 }
     29 
     30 // String returns a string in one of several forms:
     31 //
     32 //	file:line:column    valid position with file name
     33 //	line:column         valid position without file name
     34 //	file                invalid position with file name
     35 //	-                   invalid position without file name
     36 //
     37 func (pos Position) String() string {
     38 	s := pos.Filename
     39 	if pos.IsValid() {
     40 		if s != "" {
     41 			s += ":"
     42 		}
     43 		s += fmt.Sprintf("%d:%d", pos.Line, pos.Column)
     44 	}
     45 	if s == "" {
     46 		s = "-"
     47 	}
     48 	return s
     49 }
     50 
     51 // Pos is a compact encoding of a source position within a file set.
     52 // It can be converted into a Position for a more convenient, but much
     53 // larger, representation.
     54 //
     55 // The Pos value for a given file is a number in the range [base, base+size],
     56 // where base and size are specified when adding the file to the file set via
     57 // AddFile.
     58 //
     59 // To create the Pos value for a specific source offset (measured in bytes),
     60 // first add the respective file to the current file set using FileSet.AddFile
     61 // and then call File.Pos(offset) for that file. Given a Pos value p
     62 // for a specific file set fset, the corresponding Position value is
     63 // obtained by calling fset.Position(p).
     64 //
     65 // Pos values can be compared directly with the usual comparison operators:
     66 // If two Pos values p and q are in the same file, comparing p and q is
     67 // equivalent to comparing the respective source file offsets. If p and q
     68 // are in different files, p < q is true if the file implied by p was added
     69 // to the respective file set before the file implied by q.
     70 //
     71 type Pos int
     72 
     73 // The zero value for Pos is NoPos; there is no file and line information
     74 // associated with it, and NoPos().IsValid() is false. NoPos is always
     75 // smaller than any other Pos value. The corresponding Position value
     76 // for NoPos is the zero value for Position.
     77 //
     78 const NoPos Pos = 0
     79 
     80 // IsValid reports whether the position is valid.
     81 func (p Pos) IsValid() bool {
     82 	return p != NoPos
     83 }
     84 
     85 // -----------------------------------------------------------------------------
     86 // File
     87 
     88 // A File is a handle for a file belonging to a FileSet.
     89 // A File has a name, size, and line offset table.
     90 //
     91 type File struct {
     92 	set  *FileSet
     93 	name string // file name as provided to AddFile
     94 	base int    // Pos value range for this file is [base...base+size]
     95 	size int    // file size as provided to AddFile
     96 
     97 	// lines and infos are protected by set.mutex
     98 	lines []int // lines contains the offset of the first character for each line (the first entry is always 0)
     99 	infos []lineInfo
    100 }
    101 
    102 // Name returns the file name of file f as registered with AddFile.
    103 func (f *File) Name() string {
    104 	return f.name
    105 }
    106 
    107 // Base returns the base offset of file f as registered with AddFile.
    108 func (f *File) Base() int {
    109 	return f.base
    110 }
    111 
    112 // Size returns the size of file f as registered with AddFile.
    113 func (f *File) Size() int {
    114 	return f.size
    115 }
    116 
    117 // LineCount returns the number of lines in file f.
    118 func (f *File) LineCount() int {
    119 	f.set.mutex.RLock()
    120 	n := len(f.lines)
    121 	f.set.mutex.RUnlock()
    122 	return n
    123 }
    124 
    125 // AddLine adds the line offset for a new line.
    126 // The line offset must be larger than the offset for the previous line
    127 // and smaller than the file size; otherwise the line offset is ignored.
    128 //
    129 func (f *File) AddLine(offset int) {
    130 	f.set.mutex.Lock()
    131 	if i := len(f.lines); (i == 0 || f.lines[i-1] < offset) && offset < f.size {
    132 		f.lines = append(f.lines, offset)
    133 	}
    134 	f.set.mutex.Unlock()
    135 }
    136 
    137 // MergeLine merges a line with the following line. It is akin to replacing
    138 // the newline character at the end of the line with a space (to not change the
    139 // remaining offsets). To obtain the line number, consult e.g. Position.Line.
    140 // MergeLine will panic if given an invalid line number.
    141 //
    142 func (f *File) MergeLine(line int) {
    143 	if line <= 0 {
    144 		panic("illegal line number (line numbering starts at 1)")
    145 	}
    146 	f.set.mutex.Lock()
    147 	defer f.set.mutex.Unlock()
    148 	if line >= len(f.lines) {
    149 		panic("illegal line number")
    150 	}
    151 	// To merge the line numbered <line> with the line numbered <line+1>,
    152 	// we need to remove the entry in lines corresponding to the line
    153 	// numbered <line+1>. The entry in lines corresponding to the line
    154 	// numbered <line+1> is located at index <line>, since indices in lines
    155 	// are 0-based and line numbers are 1-based.
    156 	copy(f.lines[line:], f.lines[line+1:])
    157 	f.lines = f.lines[:len(f.lines)-1]
    158 }
    159 
    160 // SetLines sets the line offsets for a file and reports whether it succeeded.
    161 // The line offsets are the offsets of the first character of each line;
    162 // for instance for the content "ab\nc\n" the line offsets are {0, 3}.
    163 // An empty file has an empty line offset table.
    164 // Each line offset must be larger than the offset for the previous line
    165 // and smaller than the file size; otherwise SetLines fails and returns
    166 // false.
    167 //
    168 func (f *File) SetLines(lines []int) bool {
    169 	// verify validity of lines table
    170 	size := f.size
    171 	for i, offset := range lines {
    172 		if i > 0 && offset <= lines[i-1] || size <= offset {
    173 			return false
    174 		}
    175 	}
    176 
    177 	// set lines table
    178 	f.set.mutex.Lock()
    179 	f.lines = lines
    180 	f.set.mutex.Unlock()
    181 	return true
    182 }
    183 
    184 // SetLinesForContent sets the line offsets for the given file content.
    185 // It ignores position-altering //line comments.
    186 func (f *File) SetLinesForContent(content []byte) {
    187 	var lines []int
    188 	line := 0
    189 	for offset, b := range content {
    190 		if line >= 0 {
    191 			lines = append(lines, line)
    192 		}
    193 		line = -1
    194 		if b == '\n' {
    195 			line = offset + 1
    196 		}
    197 	}
    198 
    199 	// set lines table
    200 	f.set.mutex.Lock()
    201 	f.lines = lines
    202 	f.set.mutex.Unlock()
    203 }
    204 
    205 // A lineInfo object describes alternative file and line number
    206 // information (such as provided via a //line comment in a .go
    207 // file) for a given file offset.
    208 type lineInfo struct {
    209 	// fields are exported to make them accessible to gob
    210 	Offset   int
    211 	Filename string
    212 	Line     int
    213 }
    214 
    215 // AddLineInfo adds alternative file and line number information for
    216 // a given file offset. The offset must be larger than the offset for
    217 // the previously added alternative line info and smaller than the
    218 // file size; otherwise the information is ignored.
    219 //
    220 // AddLineInfo is typically used to register alternative position
    221 // information for //line filename:line comments in source files.
    222 //
    223 func (f *File) AddLineInfo(offset int, filename string, line int) {
    224 	f.set.mutex.Lock()
    225 	if i := len(f.infos); i == 0 || f.infos[i-1].Offset < offset && offset < f.size {
    226 		f.infos = append(f.infos, lineInfo{offset, filename, line})
    227 	}
    228 	f.set.mutex.Unlock()
    229 }
    230 
    231 // Pos returns the Pos value for the given file offset;
    232 // the offset must be <= f.Size().
    233 // f.Pos(f.Offset(p)) == p.
    234 //
    235 func (f *File) Pos(offset int) Pos {
    236 	if offset > f.size {
    237 		panic("illegal file offset")
    238 	}
    239 	return Pos(f.base + offset)
    240 }
    241 
    242 // Offset returns the offset for the given file position p;
    243 // p must be a valid Pos value in that file.
    244 // f.Offset(f.Pos(offset)) == offset.
    245 //
    246 func (f *File) Offset(p Pos) int {
    247 	if int(p) < f.base || int(p) > f.base+f.size {
    248 		panic("illegal Pos value")
    249 	}
    250 	return int(p) - f.base
    251 }
    252 
    253 // Line returns the line number for the given file position p;
    254 // p must be a Pos value in that file or NoPos.
    255 //
    256 func (f *File) Line(p Pos) int {
    257 	return f.Position(p).Line
    258 }
    259 
    260 func searchLineInfos(a []lineInfo, x int) int {
    261 	return sort.Search(len(a), func(i int) bool { return a[i].Offset > x }) - 1
    262 }
    263 
    264 // unpack returns the filename and line and column number for a file offset.
    265 // If adjusted is set, unpack will return the filename and line information
    266 // possibly adjusted by //line comments; otherwise those comments are ignored.
    267 //
    268 func (f *File) unpack(offset int, adjusted bool) (filename string, line, column int) {
    269 	filename = f.name
    270 	if i := searchInts(f.lines, offset); i >= 0 {
    271 		line, column = i+1, offset-f.lines[i]+1
    272 	}
    273 	if adjusted && len(f.infos) > 0 {
    274 		// almost no files have extra line infos
    275 		if i := searchLineInfos(f.infos, offset); i >= 0 {
    276 			alt := &f.infos[i]
    277 			filename = alt.Filename
    278 			if i := searchInts(f.lines, alt.Offset); i >= 0 {
    279 				line += alt.Line - i - 1
    280 			}
    281 		}
    282 	}
    283 	return
    284 }
    285 
    286 func (f *File) position(p Pos, adjusted bool) (pos Position) {
    287 	offset := int(p) - f.base
    288 	pos.Offset = offset
    289 	pos.Filename, pos.Line, pos.Column = f.unpack(offset, adjusted)
    290 	return
    291 }
    292 
    293 // PositionFor returns the Position value for the given file position p.
    294 // If adjusted is set, the position may be adjusted by position-altering
    295 // //line comments; otherwise those comments are ignored.
    296 // p must be a Pos value in f or NoPos.
    297 //
    298 func (f *File) PositionFor(p Pos, adjusted bool) (pos Position) {
    299 	if p != NoPos {
    300 		if int(p) < f.base || int(p) > f.base+f.size {
    301 			panic("illegal Pos value")
    302 		}
    303 		pos = f.position(p, adjusted)
    304 	}
    305 	return
    306 }
    307 
    308 // Position returns the Position value for the given file position p.
    309 // Calling f.Position(p) is equivalent to calling f.PositionFor(p, true).
    310 //
    311 func (f *File) Position(p Pos) (pos Position) {
    312 	return f.PositionFor(p, true)
    313 }
    314 
    315 // -----------------------------------------------------------------------------
    316 // FileSet
    317 
    318 // A FileSet represents a set of source files.
    319 // Methods of file sets are synchronized; multiple goroutines
    320 // may invoke them concurrently.
    321 //
    322 type FileSet struct {
    323 	mutex sync.RWMutex // protects the file set
    324 	base  int          // base offset for the next file
    325 	files []*File      // list of files in the order added to the set
    326 	last  *File        // cache of last file looked up
    327 }
    328 
    329 // NewFileSet creates a new file set.
    330 func NewFileSet() *FileSet {
    331 	return &FileSet{
    332 		base: 1, // 0 == NoPos
    333 	}
    334 }
    335 
    336 // Base returns the minimum base offset that must be provided to
    337 // AddFile when adding the next file.
    338 //
    339 func (s *FileSet) Base() int {
    340 	s.mutex.RLock()
    341 	b := s.base
    342 	s.mutex.RUnlock()
    343 	return b
    344 
    345 }
    346 
    347 // AddFile adds a new file with a given filename, base offset, and file size
    348 // to the file set s and returns the file. Multiple files may have the same
    349 // name. The base offset must not be smaller than the FileSet's Base(), and
    350 // size must not be negative. As a special case, if a negative base is provided,
    351 // the current value of the FileSet's Base() is used instead.
    352 //
    353 // Adding the file will set the file set's Base() value to base + size + 1
    354 // as the minimum base value for the next file. The following relationship
    355 // exists between a Pos value p for a given file offset offs:
    356 //
    357 //	int(p) = base + offs
    358 //
    359 // with offs in the range [0, size] and thus p in the range [base, base+size].
    360 // For convenience, File.Pos may be used to create file-specific position
    361 // values from a file offset.
    362 //
    363 func (s *FileSet) AddFile(filename string, base, size int) *File {
    364 	s.mutex.Lock()
    365 	defer s.mutex.Unlock()
    366 	if base < 0 {
    367 		base = s.base
    368 	}
    369 	if base < s.base || size < 0 {
    370 		panic("illegal base or size")
    371 	}
    372 	// base >= s.base && size >= 0
    373 	f := &File{s, filename, base, size, []int{0}, nil}
    374 	base += size + 1 // +1 because EOF also has a position
    375 	if base < 0 {
    376 		panic("token.Pos offset overflow (> 2G of source code in file set)")
    377 	}
    378 	// add the file to the file set
    379 	s.base = base
    380 	s.files = append(s.files, f)
    381 	s.last = f
    382 	return f
    383 }
    384 
    385 // Iterate calls f for the files in the file set in the order they were added
    386 // until f returns false.
    387 //
    388 func (s *FileSet) Iterate(f func(*File) bool) {
    389 	for i := 0; ; i++ {
    390 		var file *File
    391 		s.mutex.RLock()
    392 		if i < len(s.files) {
    393 			file = s.files[i]
    394 		}
    395 		s.mutex.RUnlock()
    396 		if file == nil || !f(file) {
    397 			break
    398 		}
    399 	}
    400 }
    401 
    402 func searchFiles(a []*File, x int) int {
    403 	return sort.Search(len(a), func(i int) bool { return a[i].base > x }) - 1
    404 }
    405 
    406 func (s *FileSet) file(p Pos) *File {
    407 	s.mutex.RLock()
    408 	// common case: p is in last file
    409 	if f := s.last; f != nil && f.base <= int(p) && int(p) <= f.base+f.size {
    410 		s.mutex.RUnlock()
    411 		return f
    412 	}
    413 	// p is not in last file - search all files
    414 	if i := searchFiles(s.files, int(p)); i >= 0 {
    415 		f := s.files[i]
    416 		// f.base <= int(p) by definition of searchFiles
    417 		if int(p) <= f.base+f.size {
    418 			s.mutex.RUnlock()
    419 			s.mutex.Lock()
    420 			s.last = f // race is ok - s.last is only a cache
    421 			s.mutex.Unlock()
    422 			return f
    423 		}
    424 	}
    425 	s.mutex.RUnlock()
    426 	return nil
    427 }
    428 
    429 // File returns the file that contains the position p.
    430 // If no such file is found (for instance for p == NoPos),
    431 // the result is nil.
    432 //
    433 func (s *FileSet) File(p Pos) (f *File) {
    434 	if p != NoPos {
    435 		f = s.file(p)
    436 	}
    437 	return
    438 }
    439 
    440 // PositionFor converts a Pos p in the fileset into a Position value.
    441 // If adjusted is set, the position may be adjusted by position-altering
    442 // //line comments; otherwise those comments are ignored.
    443 // p must be a Pos value in s or NoPos.
    444 //
    445 func (s *FileSet) PositionFor(p Pos, adjusted bool) (pos Position) {
    446 	if p != NoPos {
    447 		if f := s.file(p); f != nil {
    448 			pos = f.position(p, adjusted)
    449 		}
    450 	}
    451 	return
    452 }
    453 
    454 // Position converts a Pos p in the fileset into a Position value.
    455 // Calling s.Position(p) is equivalent to calling s.PositionFor(p, true).
    456 //
    457 func (s *FileSet) Position(p Pos) (pos Position) {
    458 	return s.PositionFor(p, true)
    459 }
    460 
    461 // -----------------------------------------------------------------------------
    462 // Helper functions
    463 
    464 func searchInts(a []int, x int) int {
    465 	// This function body is a manually inlined version of:
    466 	//
    467 	//   return sort.Search(len(a), func(i int) bool { return a[i] > x }) - 1
    468 	//
    469 	// With better compiler optimizations, this may not be needed in the
    470 	// future, but at the moment this change improves the go/printer
    471 	// benchmark performance by ~30%. This has a direct impact on the
    472 	// speed of gofmt and thus seems worthwhile (2011-04-29).
    473 	// TODO(gri): Remove this when compilers have caught up.
    474 	i, j := 0, len(a)
    475 	for i < j {
    476 		h := i + (j-i)/2 // avoid overflow when computing h
    477 		// i  h < j
    478 		if a[h] <= x {
    479 			i = h + 1
    480 		} else {
    481 			j = h
    482 		}
    483 	}
    484 	return i - 1
    485 }
    486