hcl: Helper methods for detecting overlaps in ranges

This is useful, for example, when printing source snippets to the terminal
as part of diagnostics, in order to detect the portion of the source code
that coincides with the subject or context of each diagnostic.
This commit is contained in:
Martin Atkins 2018-01-14 10:08:39 -08:00
parent 600e8726ec
commit 11e4972f13
2 changed files with 288 additions and 0 deletions

View File

@ -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,
}
}

185
hcl/pos_test.go Normal file
View File

@ -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()
}