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 }