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  }