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:
parent
600e8726ec
commit
11e4972f13
103
hcl/pos.go
103
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,
|
||||
}
|
||||
}
|
||||
|
185
hcl/pos_test.go
Normal file
185
hcl/pos_test.go
Normal 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()
|
||||
}
|
Loading…
Reference in New Issue
Block a user