diff --git a/hcl/pos.go b/hcl/pos.go index 3ccdfac..2ac16c7 100644 --- a/hcl/pos.go +++ b/hcl/pos.go @@ -94,3 +94,106 @@ func (r Range) String() string { ) } } + +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, + } +} diff --git a/hcl/pos_test.go b/hcl/pos_test.go new file mode 100644 index 0000000..05703c9 --- /dev/null +++ b/hcl/pos_test.go @@ -0,0 +1,185 @@ +package hcl + +import ( + "bytes" + "fmt" + "reflect" + "testing" +) + +func TestPosOverlap(t *testing.T) { + tests := []struct { + A Range + B Range + Want Range + }{ + { + Range{ // ## + Start: Pos{Byte: 2, Line: 1, Column: 3}, + End: Pos{Byte: 4, Line: 1, Column: 5}, + }, + Range{ // #### + Start: Pos{Byte: 1, Line: 1, Column: 2}, + End: Pos{Byte: 5, Line: 1, Column: 6}, + }, + Range{ // ## + Start: Pos{Byte: 2, Line: 1, Column: 3}, + End: Pos{Byte: 4, Line: 1, Column: 5}, + }, + }, + { + Range{ // #### + Start: Pos{Byte: 0, Line: 1, Column: 1}, + End: Pos{Byte: 4, Line: 1, Column: 5}, + }, + Range{ // #### + Start: Pos{Byte: 1, Line: 1, Column: 2}, + End: Pos{Byte: 5, Line: 1, Column: 6}, + }, + Range{ // ### + Start: Pos{Byte: 1, Line: 1, Column: 2}, + End: Pos{Byte: 4, Line: 1, Column: 5}, + }, + }, + { + Range{ // #### + Start: Pos{Byte: 2, Line: 1, Column: 3}, + End: Pos{Byte: 6, Line: 1, Column: 7}, + }, + Range{ // #### + Start: Pos{Byte: 1, Line: 1, Column: 2}, + End: Pos{Byte: 5, Line: 1, Column: 6}, + }, + Range{ // ### + Start: Pos{Byte: 2, Line: 1, Column: 3}, + End: Pos{Byte: 5, Line: 1, Column: 6}, + }, + }, + { + Range{ // #### + Start: Pos{Byte: 1, Line: 1, Column: 2}, + End: Pos{Byte: 5, Line: 1, Column: 6}, + }, + Range{ // ## + Start: Pos{Byte: 2, Line: 1, Column: 3}, + End: Pos{Byte: 4, Line: 1, Column: 5}, + }, + Range{ // ## + Start: Pos{Byte: 2, Line: 1, Column: 3}, + End: Pos{Byte: 4, Line: 1, Column: 5}, + }, + }, + { + Range{ // ### + Start: Pos{Byte: 1, Line: 1, Column: 2}, + End: Pos{Byte: 4, Line: 1, Column: 5}, + }, + Range{ // #### + Start: Pos{Byte: 1, Line: 1, Column: 2}, + End: Pos{Byte: 5, Line: 1, Column: 6}, + }, + Range{ // ### + Start: Pos{Byte: 1, Line: 1, Column: 2}, + End: Pos{Byte: 4, Line: 1, Column: 5}, + }, + }, + { + Range{ // ### + Start: Pos{Byte: 2, Line: 1, Column: 3}, + End: Pos{Byte: 5, Line: 1, Column: 6}, + }, + Range{ // #### + Start: Pos{Byte: 1, Line: 1, Column: 2}, + End: Pos{Byte: 5, Line: 1, Column: 6}, + }, + Range{ // ### + Start: Pos{Byte: 2, Line: 1, Column: 3}, + End: Pos{Byte: 5, Line: 1, Column: 6}, + }, + }, + { + Range{ // #### + Start: Pos{Byte: 2, Line: 1, Column: 3}, + End: Pos{Byte: 5, Line: 1, Column: 6}, + }, + Range{ // #### + Start: Pos{Byte: 2, Line: 1, Column: 3}, + End: Pos{Byte: 5, Line: 1, Column: 6}, + }, + Range{ // #### + Start: Pos{Byte: 2, Line: 1, Column: 3}, + End: Pos{Byte: 5, Line: 1, Column: 6}, + }, + }, + { + Range{ // ## + Start: Pos{Byte: 0, Line: 1, Column: 1}, + End: Pos{Byte: 2, Line: 1, Column: 3}, + }, + Range{ // ## + Start: Pos{Byte: 4, Line: 1, Column: 5}, + End: Pos{Byte: 6, Line: 1, Column: 7}, + }, + Range{ // (no overlap) + Start: Pos{Byte: 0, Line: 1, Column: 1}, + End: Pos{Byte: 0, Line: 1, Column: 1}, + }, + }, + { + Range{ // ## + Start: Pos{Byte: 4, Line: 1, Column: 5}, + End: Pos{Byte: 6, Line: 1, Column: 7}, + }, + Range{ // ## + Start: Pos{Byte: 0, Line: 1, Column: 1}, + End: Pos{Byte: 2, Line: 1, Column: 3}, + }, + Range{ // (no overlap) + Start: Pos{Byte: 4, Line: 1, Column: 5}, + End: Pos{Byte: 4, Line: 1, Column: 5}, + }, + }, + } + + for _, test := range tests { + t.Run(fmt.Sprintf("%s<=>%s", test.A, test.B), func(t *testing.T) { + got := test.A.Overlap(test.B) + if !reflect.DeepEqual(got, test.Want) { + t.Errorf( + "wrong result\nA : %-10s %s\nB : %-10s %s\ngot : %-10s %s\nwant: %-10s %s", + visRangeOffsets(test.A), test.A, + visRangeOffsets(test.B), test.B, + visRangeOffsets(got), got, + visRangeOffsets(test.Want), test.Want, + ) + } + }) + } +} + +// visRangeOffsets is a helper that produces a visual representation of the +// start and end byte offsets of the given range, which can then be stacked +// with the same for other ranges to more easily see how the ranges relate +// to one another. +func visRangeOffsets(rng Range) string { + var buf bytes.Buffer + if rng.End.Byte < rng.Start.Byte { + // Should never happen, but we'll visualize it anyway so we can + // more easily debug failing tests. + for i := 0; i < rng.End.Byte; i++ { + buf.WriteByte(' ') + } + for i := rng.End.Byte; i < rng.Start.Byte; i++ { + buf.WriteByte('!') + } + return buf.String() + } + + for i := 0; i < rng.Start.Byte; i++ { + buf.WriteByte(' ') + } + for i := rng.Start.Byte; i < rng.End.Byte; i++ { + buf.WriteByte('#') + } + return buf.String() +}