1package hcl 2 3import "fmt" 4 5// Pos represents a single position in a source file, by addressing the 6// start byte of a unicode character encoded in UTF-8. 7// 8// Pos is generally used only in the context of a Range, which then defines 9// which source file the position is within. 10type Pos struct { 11 // Line is the source code line where this position points. Lines are 12 // counted starting at 1 and incremented for each newline character 13 // encountered. 14 Line int 15 16 // Column is the source code column where this position points, in 17 // unicode characters, with counting starting at 1. 18 // 19 // Column counts characters as they appear visually, so for example a 20 // latin letter with a combining diacritic mark counts as one character. 21 // This is intended for rendering visual markers against source code in 22 // contexts where these diacritics would be rendered in a single character 23 // cell. Technically speaking, Column is counting grapheme clusters as 24 // used in unicode normalization. 25 Column int 26 27 // Byte is the byte offset into the file where the indicated character 28 // begins. This is a zero-based offset to the first byte of the first 29 // UTF-8 codepoint sequence in the character, and thus gives a position 30 // that can be resolved _without_ awareness of Unicode characters. 31 Byte int 32} 33 34// InitialPos is a suitable position to use to mark the start of a file. 35var InitialPos = Pos{Byte: 0, Line: 1, Column: 1} 36 37// Range represents a span of characters between two positions in a source 38// file. 39// 40// This struct is usually used by value in types that represent AST nodes, 41// but by pointer in types that refer to the positions of other objects, 42// such as in diagnostics. 43type Range struct { 44 // Filename is the name of the file into which this range's positions 45 // point. 46 Filename string 47 48 // Start and End represent the bounds of this range. Start is inclusive 49 // and End is exclusive. 50 Start, End Pos 51} 52 53// RangeBetween returns a new range that spans from the beginning of the 54// start range to the end of the end range. 55// 56// The result is meaningless if the two ranges do not belong to the same 57// source file or if the end range appears before the start range. 58func RangeBetween(start, end Range) Range { 59 return Range{ 60 Filename: start.Filename, 61 Start: start.Start, 62 End: end.End, 63 } 64} 65 66// RangeOver returns a new range that covers both of the given ranges and 67// possibly additional content between them if the two ranges do not overlap. 68// 69// If either range is empty then it is ignored. The result is empty if both 70// given ranges are empty. 71// 72// The result is meaningless if the two ranges to not belong to the same 73// source file. 74func RangeOver(a, b Range) Range { 75 if a.Empty() { 76 return b 77 } 78 if b.Empty() { 79 return a 80 } 81 82 var start, end Pos 83 if a.Start.Byte < b.Start.Byte { 84 start = a.Start 85 } else { 86 start = b.Start 87 } 88 if a.End.Byte > b.End.Byte { 89 end = a.End 90 } else { 91 end = b.End 92 } 93 return Range{ 94 Filename: a.Filename, 95 Start: start, 96 End: end, 97 } 98} 99 100// ContainsPos returns true if and only if the given position is contained within 101// the receiving range. 102// 103// In the unlikely case that the line/column information disagree with the byte 104// offset information in the given position or receiving range, the byte 105// offsets are given priority. 106func (r Range) ContainsPos(pos Pos) bool { 107 return r.ContainsOffset(pos.Byte) 108} 109 110// ContainsOffset returns true if and only if the given byte offset is within 111// the receiving Range. 112func (r Range) ContainsOffset(offset int) bool { 113 return offset >= r.Start.Byte && offset < r.End.Byte 114} 115 116// Ptr returns a pointer to a copy of the receiver. This is a convenience when 117// ranges in places where pointers are required, such as in Diagnostic, but 118// the range in question is returned from a method. Go would otherwise not 119// allow one to take the address of a function call. 120func (r Range) Ptr() *Range { 121 return &r 122} 123 124// String returns a compact string representation of the receiver. 125// Callers should generally prefer to present a range more visually, 126// e.g. via markers directly on the relevant portion of source code. 127func (r Range) String() string { 128 if r.Start.Line == r.End.Line { 129 return fmt.Sprintf( 130 "%s:%d,%d-%d", 131 r.Filename, 132 r.Start.Line, r.Start.Column, 133 r.End.Column, 134 ) 135 } else { 136 return fmt.Sprintf( 137 "%s:%d,%d-%d,%d", 138 r.Filename, 139 r.Start.Line, r.Start.Column, 140 r.End.Line, r.End.Column, 141 ) 142 } 143} 144 145func (r Range) Empty() bool { 146 return r.Start.Byte == r.End.Byte 147} 148 149// CanSliceBytes returns true if SliceBytes could return an accurate 150// sub-slice of the given slice. 151// 152// This effectively tests whether the start and end offsets of the range 153// are within the bounds of the slice, and thus whether SliceBytes can be 154// trusted to produce an accurate start and end position within that slice. 155func (r Range) CanSliceBytes(b []byte) bool { 156 switch { 157 case r.Start.Byte < 0 || r.Start.Byte > len(b): 158 return false 159 case r.End.Byte < 0 || r.End.Byte > len(b): 160 return false 161 case r.End.Byte < r.Start.Byte: 162 return false 163 default: 164 return true 165 } 166} 167 168// SliceBytes returns a sub-slice of the given slice that is covered by the 169// receiving range, assuming that the given slice is the source code of the 170// file indicated by r.Filename. 171// 172// If the receiver refers to any byte offsets that are outside of the slice 173// then the result is constrained to the overlapping portion only, to avoid 174// a panic. Use CanSliceBytes to determine if the result is guaranteed to 175// be an accurate span of the requested range. 176func (r Range) SliceBytes(b []byte) []byte { 177 start := r.Start.Byte 178 end := r.End.Byte 179 if start < 0 { 180 start = 0 181 } else if start > len(b) { 182 start = len(b) 183 } 184 if end < 0 { 185 end = 0 186 } else if end > len(b) { 187 end = len(b) 188 } 189 if end < start { 190 end = start 191 } 192 return b[start:end] 193} 194 195// Overlaps returns true if the receiver and the other given range share any 196// characters in common. 197func (r Range) Overlaps(other Range) bool { 198 switch { 199 case r.Filename != other.Filename: 200 // If the ranges are in different files then they can't possibly overlap 201 return false 202 case r.Empty() || other.Empty(): 203 // Empty ranges can never overlap 204 return false 205 case r.ContainsOffset(other.Start.Byte) || r.ContainsOffset(other.End.Byte): 206 return true 207 case other.ContainsOffset(r.Start.Byte) || other.ContainsOffset(r.End.Byte): 208 return true 209 default: 210 return false 211 } 212} 213 214// Overlap finds a range that is either identical to or a sub-range of both 215// the receiver and the other given range. It returns an empty range 216// within the receiver if there is no overlap between the two ranges. 217// 218// A non-empty result is either identical to or a subset of the receiver. 219func (r Range) Overlap(other Range) Range { 220 if !r.Overlaps(other) { 221 // Start == End indicates an empty range 222 return Range{ 223 Filename: r.Filename, 224 Start: r.Start, 225 End: r.Start, 226 } 227 } 228 229 var start, end Pos 230 if r.Start.Byte > other.Start.Byte { 231 start = r.Start 232 } else { 233 start = other.Start 234 } 235 if r.End.Byte < other.End.Byte { 236 end = r.End 237 } else { 238 end = other.End 239 } 240 241 return Range{ 242 Filename: r.Filename, 243 Start: start, 244 End: end, 245 } 246} 247 248// PartitionAround finds the portion of the given range that overlaps with 249// the reciever and returns three ranges: the portion of the reciever that 250// precedes the overlap, the overlap itself, and then the portion of the 251// reciever that comes after the overlap. 252// 253// If the two ranges do not overlap then all three returned ranges are empty. 254// 255// If the given range aligns with or extends beyond either extent of the 256// reciever then the corresponding outer range will be empty. 257func (r Range) PartitionAround(other Range) (before, overlap, after Range) { 258 overlap = r.Overlap(other) 259 if overlap.Empty() { 260 return overlap, overlap, overlap 261 } 262 263 before = Range{ 264 Filename: r.Filename, 265 Start: r.Start, 266 End: overlap.Start, 267 } 268 after = Range{ 269 Filename: r.Filename, 270 Start: overlap.End, 271 End: r.End, 272 } 273 274 return before, overlap, after 275} 276