From 11e4972f13e8c3707bd04351a3ffea34a531c0f7 Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Sun, 14 Jan 2018 10:08:39 -0800 Subject: [PATCH] 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. --- hcl/pos.go | 103 +++++++++++++++++++++++++++ hcl/pos_test.go | 185 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 288 insertions(+) create mode 100644 hcl/pos_test.go 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() +}