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