cuelang.org/go@v0.13.0/cue/token/position.go (about)

     1  // Copyright 2018 The CUE Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package token
    16  
    17  import (
    18  	"cmp"
    19  	"fmt"
    20  	"sort"
    21  	"sync"
    22  )
    23  
    24  // -----------------------------------------------------------------------------
    25  // Positions
    26  
    27  // Position describes an arbitrary source position
    28  // including the file, line, and column location.
    29  // A Position is valid if the line number is > 0.
    30  type Position struct {
    31  	Filename string // filename, if any
    32  	Offset   int    // offset, starting at 0
    33  	Line     int    // line number, starting at 1
    34  	Column   int    // column number, starting at 1 (byte count)
    35  	// RelPos   Pos // relative position information
    36  }
    37  
    38  // IsValid reports whether the position is valid.
    39  func (pos *Position) IsValid() bool { return pos.Line > 0 }
    40  
    41  // String returns a string in one of several forms:
    42  //
    43  //	file:line:column    valid position with file name
    44  //	line:column         valid position without file name
    45  //	file                invalid position with file name
    46  //	-                   invalid position without file name
    47  func (pos Position) String() string {
    48  	s := pos.Filename
    49  	if pos.IsValid() {
    50  		if s != "" {
    51  			s += ":"
    52  		}
    53  		s += fmt.Sprintf("%d:%d", pos.Line, pos.Column)
    54  	}
    55  	if s == "" {
    56  		s = "-"
    57  	}
    58  	return s
    59  }
    60  
    61  // Pos is a compact encoding of a source position within a file, as well as
    62  // relative positioning information. It can be converted into a Position for a
    63  // more convenient, but much larger, representation.
    64  type Pos struct {
    65  	file   *File
    66  	offset int
    67  }
    68  
    69  // File returns the file that contains the position p or nil if there is no
    70  // such file (for instance for p == NoPos).
    71  func (p Pos) File() *File {
    72  	if p.index() == 0 {
    73  		return nil
    74  	}
    75  	return p.file
    76  }
    77  
    78  // TODO(mvdan): The methods below don't need to build an entire Position
    79  // just to access some of the information. This could matter particularly for
    80  // Compare, as it is called many times when sorting by position.
    81  
    82  func (p Pos) Line() int {
    83  	if p.file == nil {
    84  		return 0
    85  	}
    86  	return p.Position().Line
    87  }
    88  
    89  func (p Pos) Column() int {
    90  	if p.file == nil {
    91  		return 0
    92  	}
    93  	return p.Position().Column
    94  }
    95  
    96  func (p Pos) Filename() string {
    97  	if p.file == nil {
    98  		return ""
    99  	}
   100  	return p.Position().Filename
   101  }
   102  
   103  func (p Pos) Position() Position {
   104  	if p.file == nil {
   105  		return Position{}
   106  	}
   107  	return p.file.Position(p)
   108  }
   109  
   110  func (p Pos) String() string {
   111  	return p.Position().String()
   112  }
   113  
   114  // Compare returns an integer comparing two positions. The result will be 0 if p == p2,
   115  // -1 if p < p2, and +1 if p > p2. Note that [NoPos] is always larger than any valid position.
   116  func (p Pos) Compare(p2 Pos) int {
   117  	if p == p2 {
   118  		return 0
   119  	} else if p == NoPos {
   120  		return +1
   121  	} else if p2 == NoPos {
   122  		return -1
   123  	}
   124  	pos, pos2 := p.Position(), p2.Position()
   125  	if c := cmp.Compare(pos.Filename, pos2.Filename); c != 0 {
   126  		return c
   127  	}
   128  	// Note that CUE doesn't currently use any directives which alter
   129  	// position information, like Go's //line, so comparing by offset is enough.
   130  	return cmp.Compare(pos.Offset, pos2.Offset)
   131  
   132  }
   133  
   134  // NoPos is the zero value for [Pos]; there is no file and line information
   135  // associated with it, and [Pos.IsValid] is false.
   136  //
   137  // NoPos is always larger than any valid [Pos] value, as it tends to relate
   138  // to values produced from evaluating existing values with valid positions.
   139  // The corresponding [Position] value for NoPos is the zero value.
   140  var NoPos = Pos{}
   141  
   142  // RelPos indicates the relative position of token to the previous token.
   143  type RelPos int
   144  
   145  //go:generate go run golang.org/x/tools/cmd/stringer -type=RelPos -linecomment
   146  
   147  const (
   148  	// NoRelPos indicates no relative position is specified.
   149  	NoRelPos RelPos = iota // invalid
   150  
   151  	// Elided indicates that the token for which this position is defined is
   152  	// not rendered at all.
   153  	Elided // elided
   154  
   155  	// NoSpace indicates there is no whitespace before this token.
   156  	NoSpace // nospace
   157  
   158  	// Blank means there is horizontal space before this token.
   159  	Blank // blank
   160  
   161  	// Newline means there is a single newline before this token.
   162  	Newline // newline
   163  
   164  	// NewSection means there are two or more newlines before this token.
   165  	NewSection // section
   166  
   167  	relMask  = 0xf
   168  	relShift = 4
   169  )
   170  
   171  func (p RelPos) Pos() Pos {
   172  	return Pos{nil, int(p)}
   173  }
   174  
   175  // HasRelPos reports whether p has a relative position.
   176  func (p Pos) HasRelPos() bool {
   177  	return p.offset&relMask != 0
   178  
   179  }
   180  
   181  func (p Pos) Before(q Pos) bool {
   182  	return p.file == q.file && p.Offset() < q.Offset()
   183  }
   184  
   185  // Offset reports the byte offset relative to the file.
   186  func (p Pos) Offset() int {
   187  	return p.Position().Offset
   188  }
   189  
   190  // Add creates a new position relative to the p offset by n.
   191  func (p Pos) Add(n int) Pos {
   192  	return Pos{p.file, p.offset + toPos(index(n))}
   193  }
   194  
   195  // IsValid reports whether the position is valid.
   196  func (p Pos) IsValid() bool {
   197  	return p != NoPos
   198  }
   199  
   200  // IsNewline reports whether the relative information suggests this node should
   201  // be printed on a new line.
   202  func (p Pos) IsNewline() bool {
   203  	return p.RelPos() >= Newline
   204  }
   205  
   206  func (p Pos) WithRel(rel RelPos) Pos {
   207  	return Pos{p.file, p.offset&^relMask | int(rel)}
   208  }
   209  
   210  func (p Pos) RelPos() RelPos {
   211  	return RelPos(p.offset & relMask)
   212  }
   213  
   214  func (p Pos) index() index {
   215  	return index(p.offset) >> relShift
   216  }
   217  
   218  func toPos(x index) int {
   219  	return (int(x) << relShift)
   220  }
   221  
   222  // -----------------------------------------------------------------------------
   223  // File
   224  
   225  // index represents an offset into the file.
   226  // It's 1-based rather than zero-based so that
   227  // we can distinguish the zero Pos from a Pos that
   228  // just has a zero offset.
   229  type index int
   230  
   231  // A File has a name, size, and line offset table.
   232  type File struct {
   233  	mutex sync.RWMutex
   234  	name  string // file name as provided to AddFile
   235  	// base is deprecated and stored only so that [File.Base]
   236  	// can continue to return the same value passed to [NewFile].
   237  	base index
   238  	size index // file size as provided to AddFile
   239  
   240  	// lines and infos are protected by set.mutex
   241  	lines []index // lines contains the offset of the first character for each line (the first entry is always 0)
   242  	infos []lineInfo
   243  }
   244  
   245  // NewFile returns a new file with the given OS file name. The size provides the
   246  // size of the whole file.
   247  //
   248  // The second argument is deprecated. It has no effect.
   249  func NewFile(filename string, deprecatedBase, size int) *File {
   250  	if deprecatedBase < 0 {
   251  		deprecatedBase = 1
   252  	}
   253  	return &File{sync.RWMutex{}, filename, index(deprecatedBase), index(size), []index{0}, nil}
   254  }
   255  
   256  // Name returns the file name of file f as registered with AddFile.
   257  func (f *File) Name() string {
   258  	return f.name
   259  }
   260  
   261  // Base returns the base offset of file f as passed to NewFile.
   262  //
   263  // Deprecated: this method just returns the (deprecated) second argument passed to NewFile.
   264  func (f *File) Base() int {
   265  	return int(f.base)
   266  }
   267  
   268  // Size returns the size of file f as passed to NewFile.
   269  func (f *File) Size() int {
   270  	return int(f.size)
   271  }
   272  
   273  // LineCount returns the number of lines in file f.
   274  func (f *File) LineCount() int {
   275  	f.mutex.RLock()
   276  	n := len(f.lines)
   277  	f.mutex.RUnlock()
   278  	return n
   279  }
   280  
   281  // AddLine adds the line offset for a new line.
   282  // The line offset must be larger than the offset for the previous line
   283  // and smaller than the file size; otherwise the line offset is ignored.
   284  func (f *File) AddLine(offset int) {
   285  	x := index(offset)
   286  	f.mutex.Lock()
   287  	if i := len(f.lines); (i == 0 || f.lines[i-1] < x) && x < f.size {
   288  		f.lines = append(f.lines, x)
   289  	}
   290  	f.mutex.Unlock()
   291  }
   292  
   293  // MergeLine merges a line with the following line. It is akin to replacing
   294  // the newline character at the end of the line with a space (to not change the
   295  // remaining offsets). To obtain the line number, consult e.g. Position.Line.
   296  // MergeLine will panic if given an invalid line number.
   297  func (f *File) MergeLine(line int) {
   298  	if line <= 0 {
   299  		panic("illegal line number (line numbering starts at 1)")
   300  	}
   301  	f.mutex.Lock()
   302  	defer f.mutex.Unlock()
   303  	if line >= len(f.lines) {
   304  		panic("illegal line number")
   305  	}
   306  	// To merge the line numbered <line> with the line numbered <line+1>,
   307  	// we need to remove the entry in lines corresponding to the line
   308  	// numbered <line+1>. The entry in lines corresponding to the line
   309  	// numbered <line+1> is located at index <line>, since indices in lines
   310  	// are 0-based and line numbers are 1-based.
   311  	copy(f.lines[line:], f.lines[line+1:])
   312  	f.lines = f.lines[:len(f.lines)-1]
   313  }
   314  
   315  // Lines returns the effective line offset table of the form described by [File.SetLines].
   316  // Callers must not mutate the result.
   317  func (f *File) Lines() []int {
   318  	var lines []int
   319  	f.mutex.Lock()
   320  	// Unfortunate that we have to loop, but we use our own type.
   321  	for _, line := range f.lines {
   322  		lines = append(lines, int(line))
   323  	}
   324  	f.mutex.Unlock()
   325  	return lines
   326  }
   327  
   328  // SetLines sets the line offsets for a file and reports whether it succeeded.
   329  // The line offsets are the offsets of the first character of each line;
   330  // for instance for the content "ab\nc\n" the line offsets are {0, 3}.
   331  // An empty file has an empty line offset table.
   332  // Each line offset must be larger than the offset for the previous line
   333  // and smaller than the file size; otherwise SetLines fails and returns
   334  // false.
   335  // Callers must not mutate the provided slice after SetLines returns.
   336  func (f *File) SetLines(lines []int) bool {
   337  	// verify validity of lines table
   338  	size := f.size
   339  	for i, offset := range lines {
   340  		if i > 0 && offset <= lines[i-1] || size <= index(offset) {
   341  			return false
   342  		}
   343  	}
   344  
   345  	// set lines table
   346  	f.mutex.Lock()
   347  	f.lines = f.lines[:0]
   348  	for _, l := range lines {
   349  		f.lines = append(f.lines, index(l))
   350  	}
   351  	f.mutex.Unlock()
   352  	return true
   353  }
   354  
   355  // SetLinesForContent sets the line offsets for the given file content.
   356  // It ignores position-altering //line comments.
   357  func (f *File) SetLinesForContent(content []byte) {
   358  	var lines []index
   359  	line := index(0)
   360  	for offset, b := range content {
   361  		if line >= 0 {
   362  			lines = append(lines, line)
   363  		}
   364  		line = -1
   365  		if b == '\n' {
   366  			line = index(offset) + 1
   367  		}
   368  	}
   369  
   370  	// set lines table
   371  	f.mutex.Lock()
   372  	f.lines = lines
   373  	f.mutex.Unlock()
   374  }
   375  
   376  // A lineInfo object describes alternative file and line number
   377  // information (such as provided via a //line comment in a .go
   378  // file) for a given file offset.
   379  type lineInfo struct {
   380  	// fields are exported to make them accessible to gob
   381  	Offset   int
   382  	Filename string
   383  	Line     int
   384  }
   385  
   386  // AddLineInfo adds alternative file and line number information for
   387  // a given file offset. The offset must be larger than the offset for
   388  // the previously added alternative line info and smaller than the
   389  // file size; otherwise the information is ignored.
   390  //
   391  // AddLineInfo is typically used to register alternative position
   392  // information for //line filename:line comments in source files.
   393  func (f *File) AddLineInfo(offset int, filename string, line int) {
   394  	x := index(offset)
   395  	f.mutex.Lock()
   396  	if i := len(f.infos); i == 0 || index(f.infos[i-1].Offset) < x && x < f.size {
   397  		f.infos = append(f.infos, lineInfo{offset, filename, line})
   398  	}
   399  	f.mutex.Unlock()
   400  }
   401  
   402  // Pos returns the Pos value for the given file offset;
   403  // the offset must be <= f.Size().
   404  // f.Pos(f.Offset(p)) == p.
   405  func (f *File) Pos(offset int, rel RelPos) Pos {
   406  	if index(offset) > f.size {
   407  		panic("illegal file offset")
   408  	}
   409  	return Pos{f, toPos(1+index(offset)) + int(rel)}
   410  }
   411  
   412  // Offset returns the offset for the given file position p;
   413  // p must be a valid Pos value in that file.
   414  // f.Offset(f.Pos(offset)) == offset.
   415  func (f *File) Offset(p Pos) int {
   416  	x := p.index()
   417  	if x < 1 || x > 1+f.size {
   418  		panic("illegal Pos value")
   419  	}
   420  	return int(x - 1)
   421  }
   422  
   423  // Line returns the line number for the given file position p;
   424  // p must be a Pos value in that file or NoPos.
   425  func (f *File) Line(p Pos) int {
   426  	return f.Position(p).Line
   427  }
   428  
   429  func searchLineInfos(a []lineInfo, x int) int {
   430  	return sort.Search(len(a), func(i int) bool { return a[i].Offset > x }) - 1
   431  }
   432  
   433  // unpack returns the filename and line and column number for a file offset.
   434  // If adjusted is set, unpack will return the filename and line information
   435  // possibly adjusted by //line comments; otherwise those comments are ignored.
   436  func (f *File) unpack(offset index, adjusted bool) (filename string, line, column int) {
   437  	filename = f.name
   438  	if i := searchInts(f.lines, offset); i >= 0 {
   439  		line, column = i+1, int(offset-f.lines[i]+1)
   440  	}
   441  	if adjusted && len(f.infos) > 0 {
   442  		// almost no files have extra line infos
   443  		if i := searchLineInfos(f.infos, int(offset)); i >= 0 {
   444  			alt := &f.infos[i]
   445  			filename = alt.Filename
   446  			if i := searchInts(f.lines, index(alt.Offset)); i >= 0 {
   447  				line += alt.Line - i - 1
   448  			}
   449  		}
   450  	}
   451  	return
   452  }
   453  
   454  func (f *File) position(p Pos, adjusted bool) (pos Position) {
   455  	offset := p.index() - 1
   456  	pos.Offset = int(offset)
   457  	pos.Filename, pos.Line, pos.Column = f.unpack(offset, adjusted)
   458  	return
   459  }
   460  
   461  // PositionFor returns the Position value for the given file position p.
   462  // If adjusted is set, the position may be adjusted by position-altering
   463  // //line comments; otherwise those comments are ignored.
   464  // p must be a Pos value in f or NoPos.
   465  func (f *File) PositionFor(p Pos, adjusted bool) (pos Position) {
   466  	x := p.index()
   467  	if p != NoPos {
   468  		if x < 1 || x > 1+f.size {
   469  			panic("illegal Pos value")
   470  		}
   471  		pos = f.position(p, adjusted)
   472  	}
   473  	return
   474  }
   475  
   476  // Position returns the Position value for the given file position p.
   477  // Calling f.Position(p) is equivalent to calling f.PositionFor(p, true).
   478  func (f *File) Position(p Pos) (pos Position) {
   479  	return f.PositionFor(p, true)
   480  }
   481  
   482  // -----------------------------------------------------------------------------
   483  // Helper functions
   484  
   485  func searchInts(a []index, x index) int {
   486  	// This function body is a manually inlined version of:
   487  	//
   488  	//   return sort.Search(len(a), func(i int) bool { return a[i] > x }) - 1
   489  	//
   490  	// With better compiler optimizations, this may not be needed in the
   491  	// future, but at the moment this change improves the go/printer
   492  	// benchmark performance by ~30%. This has a direct impact on the
   493  	// speed of gofmt and thus seems worthwhile (2011-04-29).
   494  	// TODO(gri): Remove this when compilers have caught up.
   495  	i, j := 0, len(a)
   496  	for i < j {
   497  		h := i + (j-i)/2 // avoid overflow when computing h
   498  		// i ≤ h < j
   499  		if a[h] <= x {
   500  			i = h + 1
   501  		} else {
   502  			j = h
   503  		}
   504  	}
   505  	return i - 1
   506  }