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 }