| package hcl |
| |
| import "fmt" |
| |
| // Pos represents a single position in a source file, by addressing the |
| // start byte of a unicode character encoded in UTF-8. |
| // |
| // Pos is generally used only in the context of a Range, which then defines |
| // which source file the position is within. |
| type Pos struct { |
| // Line is the source code line where this position points. Lines are |
| // counted starting at 1 and incremented for each newline character |
| // encountered. |
| Line int |
| |
| // Column is the source code column where this position points, in |
| // unicode characters, with counting starting at 1. |
| // |
| // Column counts characters as they appear visually, so for example a |
| // latin letter with a combining diacritic mark counts as one character. |
| // This is intended for rendering visual markers against source code in |
| // contexts where these diacritics would be rendered in a single character |
| // cell. Technically speaking, Column is counting grapheme clusters as |
| // used in unicode normalization. |
| Column int |
| |
| // Byte is the byte offset into the file where the indicated character |
| // begins. This is a zero-based offset to the first byte of the first |
| // UTF-8 codepoint sequence in the character, and thus gives a position |
| // that can be resolved _without_ awareness of Unicode characters. |
| Byte int |
| } |
| |
| // InitialPos is a suitable position to use to mark the start of a file. |
| var InitialPos = Pos{Byte: 0, Line: 1, Column: 1} |
| |
| // Range represents a span of characters between two positions in a source |
| // file. |
| // |
| // This struct is usually used by value in types that represent AST nodes, |
| // but by pointer in types that refer to the positions of other objects, |
| // such as in diagnostics. |
| type Range struct { |
| // Filename is the name of the file into which this range's positions |
| // point. |
| Filename string |
| |
| // Start and End represent the bounds of this range. Start is inclusive |
| // and End is exclusive. |
| Start, End Pos |
| } |
| |
| // RangeBetween returns a new range that spans from the beginning of the |
| // start range to the end of the end range. |
| // |
| // The result is meaningless if the two ranges do not belong to the same |
| // source file or if the end range appears before the start range. |
| func RangeBetween(start, end Range) Range { |
| return Range{ |
| Filename: start.Filename, |
| Start: start.Start, |
| End: end.End, |
| } |
| } |
| |
| // RangeOver returns a new range that covers both of the given ranges and |
| // possibly additional content between them if the two ranges do not overlap. |
| // |
| // If either range is empty then it is ignored. The result is empty if both |
| // given ranges are empty. |
| // |
| // The result is meaningless if the two ranges to not belong to the same |
| // source file. |
| func RangeOver(a, b Range) Range { |
| if a.Empty() { |
| return b |
| } |
| if b.Empty() { |
| return a |
| } |
| |
| var start, end Pos |
| if a.Start.Byte < b.Start.Byte { |
| start = a.Start |
| } else { |
| start = b.Start |
| } |
| if a.End.Byte > b.End.Byte { |
| end = a.End |
| } else { |
| end = b.End |
| } |
| return Range{ |
| Filename: a.Filename, |
| Start: start, |
| End: end, |
| } |
| } |
| |
| // ContainsPos returns true if and only if the given position is contained within |
| // the receiving range. |
| // |
| // In the unlikely case that the line/column information disagree with the byte |
| // offset information in the given position or receiving range, the byte |
| // offsets are given priority. |
| func (r Range) ContainsPos(pos Pos) bool { |
| return r.ContainsOffset(pos.Byte) |
| } |
| |
| // ContainsOffset returns true if and only if the given byte offset is within |
| // the receiving Range. |
| func (r Range) ContainsOffset(offset int) bool { |
| return offset >= r.Start.Byte && offset < r.End.Byte |
| } |
| |
| // Ptr returns a pointer to a copy of the receiver. This is a convenience when |
| // ranges in places where pointers are required, such as in Diagnostic, but |
| // the range in question is returned from a method. Go would otherwise not |
| // allow one to take the address of a function call. |
| func (r Range) Ptr() *Range { |
| return &r |
| } |
| |
| // String returns a compact string representation of the receiver. |
| // Callers should generally prefer to present a range more visually, |
| // e.g. via markers directly on the relevant portion of source code. |
| func (r Range) String() string { |
| if r.Start.Line == r.End.Line { |
| return fmt.Sprintf( |
| "%s:%d,%d-%d", |
| r.Filename, |
| r.Start.Line, r.Start.Column, |
| r.End.Column, |
| ) |
| } else { |
| return fmt.Sprintf( |
| "%s:%d,%d-%d,%d", |
| r.Filename, |
| r.Start.Line, r.Start.Column, |
| r.End.Line, r.End.Column, |
| ) |
| } |
| } |
| |
| func (r Range) Empty() bool { |
| return r.Start.Byte == r.End.Byte |
| } |
| |
| // CanSliceBytes returns true if SliceBytes could return an accurate |
| // sub-slice of the given slice. |
| // |
| // This effectively tests whether the start and end offsets of the range |
| // are within the bounds of the slice, and thus whether SliceBytes can be |
| // trusted to produce an accurate start and end position within that slice. |
| func (r Range) CanSliceBytes(b []byte) bool { |
| switch { |
| case r.Start.Byte < 0 || r.Start.Byte > len(b): |
| return false |
| case r.End.Byte < 0 || r.End.Byte > len(b): |
| return false |
| case r.End.Byte < r.Start.Byte: |
| return false |
| default: |
| return true |
| } |
| } |
| |
| // SliceBytes returns a sub-slice of the given slice that is covered by the |
| // receiving range, assuming that the given slice is the source code of the |
| // file indicated by r.Filename. |
| // |
| // If the receiver refers to any byte offsets that are outside of the slice |
| // then the result is constrained to the overlapping portion only, to avoid |
| // a panic. Use CanSliceBytes to determine if the result is guaranteed to |
| // be an accurate span of the requested range. |
| func (r Range) SliceBytes(b []byte) []byte { |
| start := r.Start.Byte |
| end := r.End.Byte |
| if start < 0 { |
| start = 0 |
| } else if start > len(b) { |
| start = len(b) |
| } |
| if end < 0 { |
| end = 0 |
| } else if end > len(b) { |
| end = len(b) |
| } |
| if end < start { |
| end = start |
| } |
| return b[start:end] |
| } |
| |
| // Overlaps returns true if the receiver and the other given range share any |
| // characters in common. |
| func (r Range) Overlaps(other Range) bool { |
| switch { |
| case r.Filename != other.Filename: |
| // If the ranges are in different files then they can't possibly overlap |
| return false |
| case r.Empty() || other.Empty(): |
| // Empty ranges can never overlap |
| return false |
| case r.ContainsOffset(other.Start.Byte) || r.ContainsOffset(other.End.Byte): |
| return true |
| case other.ContainsOffset(r.Start.Byte) || other.ContainsOffset(r.End.Byte): |
| return true |
| default: |
| return false |
| } |
| } |
| |
| // Overlap finds a range that is either identical to or a sub-range of both |
| // the receiver and the other given range. It returns an empty range |
| // within the receiver if there is no overlap between the two ranges. |
| // |
| // A non-empty result is either identical to or a subset of the receiver. |
| func (r Range) Overlap(other Range) Range { |
| if !r.Overlaps(other) { |
| // Start == End indicates an empty range |
| return Range{ |
| Filename: r.Filename, |
| Start: r.Start, |
| End: r.Start, |
| } |
| } |
| |
| var start, end Pos |
| if r.Start.Byte > other.Start.Byte { |
| start = r.Start |
| } else { |
| start = other.Start |
| } |
| if r.End.Byte < other.End.Byte { |
| end = r.End |
| } else { |
| end = other.End |
| } |
| |
| return Range{ |
| Filename: r.Filename, |
| Start: start, |
| End: end, |
| } |
| } |
| |
| // PartitionAround finds the portion of the given range that overlaps with |
| // the reciever and returns three ranges: the portion of the reciever that |
| // precedes the overlap, the overlap itself, and then the portion of the |
| // reciever that comes after the overlap. |
| // |
| // If the two ranges do not overlap then all three returned ranges are empty. |
| // |
| // If the given range aligns with or extends beyond either extent of the |
| // reciever then the corresponding outer range will be empty. |
| func (r Range) PartitionAround(other Range) (before, overlap, after Range) { |
| overlap = r.Overlap(other) |
| if overlap.Empty() { |
| return overlap, overlap, overlap |
| } |
| |
| before = Range{ |
| Filename: r.Filename, |
| Start: r.Start, |
| End: overlap.Start, |
| } |
| after = Range{ |
| Filename: r.Filename, |
| Start: overlap.End, |
| End: r.End, |
| } |
| |
| return before, overlap, after |
| } |