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