diff --git a/CHANGELOG.md b/CHANGELOG.md index 1cb340b5..41702b24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,16 @@ SCIP schema: - Added documentation that ranges must be half-open intervals. +Go SCIP bindings: + +- Breaking changes: + - The `NewRange` function does well-formedness checks and returns `(Range, error)` instead of `*Range`. + When skipping checks, `NewRangeUnchecked` can be used instead. + - The `SortRanges` function takes a `[]Range` instead of a `[]*Range` + to avoid extra heap allocations. +- Features: + - Added new methods for `Range` and `Position` types. + ## v0.3.3 SCIP schema: diff --git a/bindings/go/scip/canonicalize.go b/bindings/go/scip/canonicalize.go index 28a519eb..bcd469c1 100644 --- a/bindings/go/scip/canonicalize.go +++ b/bindings/go/scip/canonicalize.go @@ -36,7 +36,7 @@ func RemoveIllegalOccurrences(occurrences []*Occurrence) []*Occurrence { // CanonicalizeOccurrence deterministically re-orders the fields of the given occurrence. func CanonicalizeOccurrence(occurrence *Occurrence) *Occurrence { // Express ranges as three-components if possible - occurrence.Range = NewRange(occurrence.Range).SCIPRange() + occurrence.Range = NewRangeUnchecked(occurrence.Range).SCIPRange() occurrence.Diagnostics = CanonicalizeDiagnostics(occurrence.Diagnostics) return occurrence } diff --git a/bindings/go/scip/position.go b/bindings/go/scip/position.go index 0f7862d0..b943aa0a 100644 --- a/bindings/go/scip/position.go +++ b/bindings/go/scip/position.go @@ -1,6 +1,9 @@ package scip -// Range represents a range between two offset positions. +import "fmt" + +// Range represents [start, end) between two offset positions. +// // NOTE: the github.com/sourcegraph/sourcegraph/lib/codeintel/lsif/protocol package // contains similarly shaped structs but this one exists primarily to make it // easier to work with SCIP encoded positions, which have the type []int32 @@ -16,18 +19,109 @@ type Position struct { Character int32 } -// NewRange converts an SCIP range into `Range` -func NewRange(scipRange []int32) *Range { - var endLine int32 - var endCharacter int32 - if len(scipRange) == 3 { // single line - endLine = scipRange[0] - endCharacter = scipRange[2] - } else if len(scipRange) == 4 { // multi-line +func (p Position) Compare(other Position) int { + if p.Line < other.Line { + return -1 + } + if p.Line > other.Line { + return 1 + } + if p.Character < other.Character { + return -1 + } + if p.Character > other.Character { + return 1 + } + return 0 +} + +func (p Position) Less(other Position) bool { + if p.Line < other.Line { + return true + } + if p.Line > other.Line { + return false + } + return p.Character < other.Character +} + +//go:noinline +func makeNewRangeError(startLine, endLine, startChar, endChar int32) (Range, error) { + if startLine < 0 || endLine < 0 || startChar < 0 || endChar < 0 { + return Range{}, NegativeOffsetsRangeError + } + if startLine > endLine || (startLine == endLine && startChar > endChar) { + return Range{}, EndBeforeStartRangeError + } + panic("unreachable") +} + +// NewRange constructs a Range while checking if the input is valid. +func NewRange(scipRange []int32) (Range, error) { + // N.B. This function is kept small so that it can be inlined easily. + // See also: https://github.com/golang/go/issues/17566 + var startLine, endLine, startChar, endChar int32 + switch len(scipRange) { + case 3: + startLine = scipRange[0] + endLine = startLine + startChar = scipRange[1] + endChar = scipRange[2] + if startLine >= 0 && startChar >= 0 && endChar >= startChar { + break + } + return makeNewRangeError(startLine, endLine, startChar, endChar) + case 4: + startLine = scipRange[0] + startChar = scipRange[1] endLine = scipRange[2] + endChar = scipRange[3] + if startLine >= 0 && startChar >= 0 && + ((endLine > startLine && endChar >= 0) || (endLine == startLine && endChar >= startChar)) { + break + } + return makeNewRangeError(startLine, endLine, startChar, endChar) + default: + return Range{}, IncorrectLengthRangeError + } + return Range{Start: Position{Line: startLine, Character: startChar}, End: Position{Line: endLine, Character: endChar}}, nil +} + +type RangeError int32 + +const ( + IncorrectLengthRangeError RangeError = iota + NegativeOffsetsRangeError + EndBeforeStartRangeError +) + +var _ error = RangeError(0) + +func (e RangeError) Error() string { + switch e { + case IncorrectLengthRangeError: + return "incorrect length" + case NegativeOffsetsRangeError: + return "negative offsets" + case EndBeforeStartRangeError: + return "end before start" + } + panic("unhandled range error") +} + +// NewRangeUnchecked converts an SCIP range into `Range` +// +// Pre-condition: The input slice must follow the SCIP range encoding. +// https://sourcegraph.com/github.com/sourcegraph/scip/-/blob/scip.proto?L646:18-646:23 +func NewRangeUnchecked(scipRange []int32) Range { + // Single-line case is most common + endCharacter := scipRange[2] + endLine := scipRange[0] + if len(scipRange) == 4 { // multi-line endCharacter = scipRange[3] + endLine = scipRange[2] } - return &Range{ + return Range{ Start: Position{ Line: scipRange[0], Character: scipRange[1], @@ -49,3 +143,57 @@ func (r Range) SCIPRange() []int32 { } return []int32{r.Start.Line, r.Start.Character, r.End.Line, r.End.Character} } + +// Contains checks if position is within the range +func (r Range) Contains(position Position) bool { + return !position.Less(r.Start) && position.Less(r.End) +} + +// Intersects checks if two ranges intersect. +// +// case 1: r1.Start >= other.Start && r1.Start < other.End +// case 2: r2.Start >= r1.Start && r2.Start < r1.End +func (r Range) Intersects(other Range) bool { + return r.Start.Less(other.End) && other.Start.Less(r.End) +} + +// Compare compares two ranges. +// +// Returns 0 if the ranges intersect (not just if they're equal). +func (r Range) Compare(other Range) int { + if r.Intersects(other) { + return 0 + } + return r.Start.Compare(other.Start) +} + +// Less compares two ranges, consistent with Compare. +// +// r.Compare(other) < 0 iff r.Less(other). +func (r Range) Less(other Range) bool { + return r.End.Compare(other.Start) <= 0 +} + +// CompareStrict compares two ranges. +// +// Returns 0 iff the ranges are exactly equal. +func (r Range) CompareStrict(other Range) int { + if ret := r.Start.Compare(other.Start); ret != 0 { + return ret + } + return r.End.Compare(other.End) +} + +// LessStrict compares two ranges, consistent with CompareStrict. +// +// r.CompareStrict(other) < 0 iff r.LessStrict(other). +func (r Range) LessStrict(other Range) bool { + if ret := r.Start.Compare(other.Start); ret != 0 { + return ret < 0 + } + return r.End.Less(other.End) +} + +func (r Range) String() string { + return fmt.Sprintf("%d:%d-%d:%d", r.Start.Line, r.Start.Character, r.End.Line, r.End.Character) +} diff --git a/bindings/go/scip/position_test.go b/bindings/go/scip/position_test.go new file mode 100644 index 00000000..a5f33540 --- /dev/null +++ b/bindings/go/scip/position_test.go @@ -0,0 +1,255 @@ +package scip + +import ( + "testing" + + "github.com/hexops/autogold/v2" + + "github.com/stretchr/testify/require" + "pgregory.net/rapid" +) + +// TODO: Replace with cmp.Compare go 1.21 or newer. +func compare(i1 int, i2 int) int { + if i1 < i2 { + return -1 + } + if i1 > i2 { + return 1 + } + return 0 +} + +func TestUnitComparePosition(t *testing.T) { + positions := []Position{ + {0, 0}, + {0, 1}, + {1, 0}, + {1, 1}, + {1, 2}, + {2, 1}, + {2, 2}, + } + + type testCase struct { + p1 Position + p2 Position + expected int + } + + testCases := make([]testCase, len(positions)*len(positions)) + + // There is a total ordering on these positions, so we just test all possible combinations + for i1, pos1 := range positions { + for i2, pos2 := range positions { + testCases = append(testCases, testCase{pos1, pos2, compare(i1, i2)}) + } + } + + for _, testCase := range testCases { + if actual := testCase.p1.Compare(testCase.p2); actual != testCase.expected { + t.Errorf("unexpected result. %+v.Compare(%+v) want=%d have=%d", testCase.p1, testCase.p2, testCase.expected, actual) + } + } +} + +func TestUnitIntersect(t *testing.T) { + r1 := Range{Position{0, 0}, Position{0, 10}} + r2 := Range{Position{0, 5}, Position{1, 5}} + r3 := Range{Position{1, 0}, Position{1, 10}} + + require.Truef(t, r1.Intersects(r2), "%+v.Intersects(%+v)", r1, r2) + require.Truef(t, r2.Intersects(r3), "%+v.Intersects(%+v)", r2, r3) + require.Falsef(t, r1.Intersects(r3), "%+v.Intersects(%+v)", r1, r3) +} + +func genPosition() *rapid.Generator[Position] { + return rapid.Custom(func(t *rapid.T) Position { + return Position{ + Line: rapid.Int32Range(0, 10).Draw(t, "Line"), + Character: rapid.Int32Range(0, 10).Draw(t, "Character"), + } + }) +} + +func genRange() *rapid.Generator[Range] { + return rapid.Custom(func(t *rapid.T) Range { + posGen := genPosition() + start := posGen.Draw(t, "start") + end := posGen.Draw(t, "end") + // A well formed range is always start <= end. + if start.Compare(end) > 0 { + start, end = end, start + } + return Range{ + Start: start, + End: end, + } + }) +} + +func TestComparePositionTransitive(t *testing.T) { + rapid.Check(t, func(t *rapid.T) { + p1 := genPosition().Draw(t, "p1") + p2 := genPosition().Draw(t, "p2") + p3 := genPosition().Draw(t, "p3") + + if p1.Compare(p2) < 0 && p2.Compare(p3) < 0 { + if !(p1.Compare(p3) < 0) { + t.Errorf("%+v < %+v < %+v but !(%+v < %+v)", p1, p2, p3, p1, p3) + } + } + }) +} + +func TestIntersects(t *testing.T) { + rapid.Check(t, func(t *rapid.T) { + r1 := genRange().Draw(t, "r1") + r2 := genRange().Draw(t, "r2") + + if r1.Intersects(r2) { + if !(r1.Contains(r2.Start) || r2.Contains(r1.Start)) { + t.Errorf("%+v overlaps with %+v but neither contains the others start position", r1, r2) + } + } + }) +} + +func TestLessCompareConsistent(t *testing.T) { + rapid.Check(t, func(t *rapid.T) { + r1 := genRange().Draw(t, "r1") + r2 := genRange().Draw(t, "r2") + if r1.Compare(r2) < 0 { + if !(r1.Less(r2)) { + t.Errorf("%+v.Compare(%+v) < 0 but !(%+v < %+v)", r1, r2, r1, r2) + } + } else { + if r1.Less(r2) { + t.Errorf("%+v.Compare(%+v) >= 0 but (%+v < %+v)", r1, r2, r1, r2) + } + } + }) + + rapid.Check(t, func(t *rapid.T) { + r1 := genRange().Draw(t, "r1") + r2 := genRange().Draw(t, "r2") + if r1.CompareStrict(r2) < 0 { + if !(r1.LessStrict(r2)) { + t.Errorf("%+v.CompareStrict(%+v) < 0 but !(%+v < %+v)", r1, r2, r1, r2) + } + } else { + if r1.LessStrict(r2) { + t.Errorf("%+v.CompareStrict(%+v) >= 0 but (%+v < %+v)", r1, r2, r1, r2) + } + } + }) +} + +func TestCompareReverse(t *testing.T) { + rapid.Check(t, func(t *rapid.T) { + r1 := genRange().Draw(t, "r1") + r2 := genRange().Draw(t, "r2") + cmp1 := r1.Compare(r2) + cmp2 := r2.Compare(r1) + if cmp1 != -cmp2 { + t.Errorf("%+v.Compare(%+v) = %d but %+v.Compare(%+v) = %d", r1, r2, cmp1, r2, r1, cmp2) + } + }) +} + +func TestNewRange_UnitTests(t *testing.T) { + type testCase struct { + input []int32 + expected autogold.Value + } + type result struct { + range_ Range + err string + } + testCases := []testCase{ + { + input: nil, + expected: autogold.Expect(result{err: "incorrect length"}), + }, + { + input: []int32{3}, + expected: autogold.Expect(result{err: "incorrect length"}), + }, + { + input: []int32{3, 3}, + expected: autogold.Expect(result{err: "incorrect length"}), + }, + { + input: []int32{12, 0, 14}, + expected: autogold.Expect(result{range_: Range{ + Start: Position{Line: 12}, + End: Position{ + Line: 12, + Character: 14, + }, + }}), + }, + { + input: []int32{12, 14, 0}, + expected: autogold.Expect(result{err: "end before start"}), + }, + { + input: []int32{12, 14, 0}, + expected: autogold.Expect(result{err: "end before start"}), + }, + { + input: []int32{12, 15, 14, 0}, + expected: autogold.Expect(result{range_: Range{ + Start: Position{ + Line: 12, + Character: 15, + }, + End: Position{Line: 14}, + }}), + }, + { + input: []int32{12, 0, 12, 0}, + expected: autogold.Expect(result{range_: Range{ + Start: Position{Line: 12}, + End: Position{Line: 12}, + }}), + }, + { + input: []int32{12, 0, 13, 1, 4}, + expected: autogold.Expect(result{err: "incorrect length"}), + }, + { + input: []int32{3, -1, 4}, + expected: autogold.Expect(result{err: "negative offsets"}), + }, + } + + for _, tc := range testCases { + require.NotPanicsf(t, func() { + r, err := NewRange(tc.input) + errStr := "" + if err != nil { + errStr = err.Error() + } + tc.expected.Equal(t, result{range_: r, err: errStr}) + }, "panicked for input: %v", tc.input) + } +} + +func TestNewRange(t *testing.T) { + rapid.Check(t, func(t *rapid.T) { + input := rapid.SliceOfN(rapid.Int32Range(-2, 10), 0, 5).Draw(t, "input") + var r1 Range + var err error + require.NotPanics(t, func() { + r1, err = NewRange(input) + }) + if err != nil { + return + } + r2 := NewRangeUnchecked(input) + if r1.CompareStrict(r2) != 0 { + t.Fatalf("NewRange vs NewRangeUnchecked: %s %s", r1, r2) + } + }) +} diff --git a/bindings/go/scip/sort.go b/bindings/go/scip/sort.go index d2e42d54..e8d145d3 100644 --- a/bindings/go/scip/sort.go +++ b/bindings/go/scip/sort.go @@ -31,15 +31,16 @@ func SortDocuments(documents []*Document) []*Document { // occurrences are properly enclosed by later occurrences. func FindOccurrences(occurrences []*Occurrence, targetLine, targetCharacter int32) []*Occurrence { var filtered []*Occurrence + pos := Position{targetLine, targetCharacter} for _, occurrence := range occurrences { - if compareRanges(occurrence.Range, targetLine, targetCharacter) == 0 { + if NewRangeUnchecked(occurrence.Range).Contains(pos) { filtered = append(filtered, occurrence) } } sort.Slice(filtered, func(i, j int) bool { - // Ordered so that the least precise (largest) range comes first - return compareRanges(filtered[i].Range, filtered[j].Range...) > 0 + // Ordered so that the least precise (largest) range comes last + return NewRangeUnchecked(filtered[i].Range).CompareStrict(NewRangeUnchecked(filtered[j].Range)) > 0 }) return filtered @@ -51,11 +52,12 @@ func FindOccurrences(occurrences []*Occurrence, targetLine, targetCharacter int3 // occurrences are sorted by symbol name. func SortOccurrences(occurrences []*Occurrence) []*Occurrence { sort.Slice(occurrences, func(i, j int) bool { - if rawRangesEqual(occurrences[i].Range, occurrences[j].Range) { - return occurrences[i].Symbol < occurrences[j].Symbol + r1 := NewRangeUnchecked(occurrences[i].Range) + r2 := NewRangeUnchecked(occurrences[j].Range) + if ret := r1.CompareStrict(r2); ret != 0 { + return ret < 0 } - - return compareRanges(occurrences[i].Range, occurrences[j].Range...) <= 0 + return occurrences[i].Symbol < occurrences[j].Symbol }) return occurrences @@ -73,26 +75,18 @@ func rawRangesEqual(a, b []int32) bool { return true } - ra := NewRange(a) - rb := NewRange(b) + ra := NewRangeUnchecked(a) + rb := NewRangeUnchecked(b) return ra.Start.Line == rb.Start.Line && ra.Start.Character == rb.Start.Character && ra.End.Line == rb.End.Line && ra.End.Character == rb.End.Character } // SortRanges sorts the given range slice (in-place) and returns it (for convenience). Ranges are // sorted in ascending order of starting position, where enclosing ranges come before the enclosed. -func SortRanges(ranges []*Range) []*Range { +func SortRanges(ranges []Range) []Range { sort.Slice(ranges, func(i, j int) bool { - return comparePositionToRange( - ranges[i].Start.Line, - ranges[i].Start.Character, - ranges[i].End.Line, - ranges[i].End.Character, - ranges[j].Start.Line, - ranges[j].Start.Character, - ) <= 0 + return ranges[i].LessStrict(ranges[j]) }) - return ranges } @@ -141,39 +135,6 @@ func SortRelationships(relationships []*Relationship) []*Relationship { return relationships } -// compareRanges compares the order of the leading edge of the two ranges. This method returns -// -// - -1 if the leading edge of r2 occurs before r1, -// - +1 if the leading edge of r2 occurs after r1, and -// - +0 if the leading edge of r2 is enclosed by r1. -// -// Note that ranges are half-closed intervals, so a match on the leading end of the range will -// be considered enclosed, but a match on the trailing edge will not. -func compareRanges(r1 []int32, r2 ...int32) int { - startLine, startCharacter, endLine, endCharacter := unpackRange(r1) - - return comparePositionToRange( - startLine, - startCharacter, - endLine, - endCharacter, - r2[0], - r2[1], - ) -} - -// unpackRange unpacks the raw SCIP range into a four-element range bound. This function -// duplicates some of the logic in the SCIP repository, but we're dealing heavily with raw -// encoded proto messages in the database layer here as well, and we'd like to avoid boxing -// into a Range unnecessarily. -func unpackRange(r []int32) (int32, int32, int32, int32) { - if len(r) == 3 { - return r[0], r[1], r[0], r[2] - } - - return r[0], r[1], r[2], r[3] -} - // comparePositionToRange compares the given target position represented by line and character // against the four-element range bound. This method returns // diff --git a/bindings/go/scip/sort_test.go b/bindings/go/scip/sort_test.go index be0119d6..a783e3ab 100644 --- a/bindings/go/scip/sort_test.go +++ b/bindings/go/scip/sort_test.go @@ -77,23 +77,23 @@ func TestSortOccurrences(t *testing.T) { } func TestSortRanges(t *testing.T) { - occurrences := []*Range{ - NewRange([]int32{2, 3, 5}), // rank 2 - NewRange([]int32{11, 10, 12}), // rank 10 - NewRange([]int32{6, 3, 5}), // rank 4 - NewRange([]int32{10, 4, 8}), // rank 6 - NewRange([]int32{10, 10, 12}), // rank 7 - NewRange([]int32{0, 3, 4, 5}), // rank 0 - NewRange([]int32{12, 1, 13, 12}), // rank 11 - NewRange([]int32{11, 1, 3}), // rank 8 - NewRange([]int32{5, 3, 5}), // rank 3 - NewRange([]int32{10, 1, 3}), // rank 5 - NewRange([]int32{12, 10, 13, 3}), // rank 13 - NewRange([]int32{11, 4, 8}), // rank 9 - NewRange([]int32{12, 4, 13, 8}), // rank 12 - NewRange([]int32{1, 3, 3, 5}), // rank 1 + occurrences := []Range{ + NewRangeUnchecked([]int32{2, 3, 5}), // rank 2 + NewRangeUnchecked([]int32{11, 10, 12}), // rank 10 + NewRangeUnchecked([]int32{6, 3, 5}), // rank 4 + NewRangeUnchecked([]int32{10, 4, 8}), // rank 6 + NewRangeUnchecked([]int32{10, 10, 12}), // rank 7 + NewRangeUnchecked([]int32{0, 3, 4, 5}), // rank 0 + NewRangeUnchecked([]int32{12, 1, 13, 12}), // rank 11 + NewRangeUnchecked([]int32{11, 1, 3}), // rank 8 + NewRangeUnchecked([]int32{5, 3, 5}), // rank 3 + NewRangeUnchecked([]int32{10, 1, 3}), // rank 5 + NewRangeUnchecked([]int32{12, 10, 13, 3}), // rank 13 + NewRangeUnchecked([]int32{11, 4, 8}), // rank 9 + NewRangeUnchecked([]int32{12, 4, 13, 8}), // rank 12 + NewRangeUnchecked([]int32{1, 3, 3, 5}), // rank 1 } - unsorted := make([]*Range, len(occurrences)) + unsorted := make([]Range, len(occurrences)) copy(unsorted, occurrences) ranges := [][]int32{} @@ -122,25 +122,3 @@ func TestSortRanges(t *testing.T) { t.Errorf("unexpected occurrence order (-want +got):\n%s", diff) } } - -func TestComparePositionToRange(t *testing.T) { - testCases := []struct { - line int32 - character int32 - expected int - }{ - {5, 11, 0}, - {5, 12, 0}, - {5, 13, -1}, - {4, 12, +1}, - {5, 10, +1}, - {5, 14, -1}, - {6, 12, -1}, - } - - for _, testCase := range testCases { - if cmpRes := comparePositionToRange(5, 11, 5, 13, testCase.line, testCase.character); cmpRes != testCase.expected { - t.Errorf("unexpected ComparePositionSCIP result for %d:%d. want=%d have=%d", testCase.line, testCase.character, testCase.expected, cmpRes) - } - } -} diff --git a/bindings/go/scip/testutil/format.go b/bindings/go/scip/testutil/format.go index 9ce4fc75..ffd6db78 100644 --- a/bindings/go/scip/testutil/format.go +++ b/bindings/go/scip/testutil/format.go @@ -94,7 +94,7 @@ func FormatSnapshot( b.WriteString("\n") for i < len(document.Occurrences) && document.Occurrences[i].Range[0] == int32(lineNumber) { occ := document.Occurrences[i] - pos := scip.NewRange(occ.Range) + pos := scip.NewRangeUnchecked(occ.Range) if !pos.IsSingleLine() { i++ continue diff --git a/cmd/scip/lint.go b/cmd/scip/lint.go index 88dd8e58..f6118aac 100644 --- a/cmd/scip/lint.go +++ b/cmd/scip/lint.go @@ -113,7 +113,7 @@ type occurrenceKey struct { } func scipOccurrenceKey(occ *scip.Occurrence) occurrenceKey { - return occurrenceKey{*scip.NewRange(occ.Range), occ.SymbolRoles} + return occurrenceKey{scip.NewRangeUnchecked(occ.Range), occ.SymbolRoles} } type occurrenceMap = map[occurrenceKey]*scip.Occurrence @@ -202,16 +202,16 @@ func (st *symbolTable) addRelationship(sym string, path string, rel *scip.Relati func (st *symbolTable) addOccurrence(path string, occ *scip.Occurrence) error { if occ.Symbol == "" { - return emptyStringError{what: "symbol", context: fmt.Sprintf("occurrence at %s @ %s", path, scipRangeToString(*scip.NewRange(occ.Range)))} + return emptyStringError{what: "symbol", context: fmt.Sprintf("occurrence at %s @ %s", path, scipRangeToString(scip.NewRangeUnchecked(occ.Range)))} } if scip.SymbolRole_Definition.Matches(occ) && scip.SymbolRole_ForwardDefinition.Matches(occ) { - return forwardDefIsDefinitionError{occ.Symbol, path, *scip.NewRange(occ.Range)} + return forwardDefIsDefinitionError{occ.Symbol, path, scip.NewRangeUnchecked(occ.Range)} } tryInsertOccurrence := func(occMap fileOccurrenceMap) error { occKey := scipOccurrenceKey(occ) if fileOccs, ok := occMap[path]; ok { if _, ok := fileOccs[occKey]; ok { - return duplicateOccurrenceWarning{occ.Symbol, path, *scip.NewRange(occ.Range), occ.SymbolRoles} + return duplicateOccurrenceWarning{occ.Symbol, path, scip.NewRangeUnchecked(occ.Range), occ.SymbolRoles} } else { fileOccs[occKey] = occ } @@ -231,7 +231,7 @@ func (st *symbolTable) addOccurrence(path string, occ *scip.Occurrence) error { return err } } else { - return missingSymbolForOccurrenceError{occ.Symbol, path, *scip.NewRange(occ.Range)} + return missingSymbolForOccurrenceError{occ.Symbol, path, scip.NewRangeUnchecked(occ.Range)} } return nil } diff --git a/cmd/scip/lint_test.go b/cmd/scip/lint_test.go index 3b965192..72a54da5 100644 --- a/cmd/scip/lint_test.go +++ b/cmd/scip/lint_test.go @@ -167,15 +167,15 @@ func TestErrors(t *testing.T) { "missingSymbolForOccurrence", makeIndex(nil, nil, stringMap{"f": {"a"}}), []error{ - missingSymbolForOccurrenceError{"a", "f", *scip.NewRange(placeholderRange)}, + missingSymbolForOccurrenceError{"a", "f", scip.NewRangeUnchecked(placeholderRange)}, }, }, { "duplicateOccurrence", makeIndex([]string{"a"}, stringMap{"f": {"b"}}, stringMap{"f": {"a", "a", "b", "b"}}), []error{ - duplicateOccurrenceWarning{"a", "f", *scip.NewRange(placeholderRange), placeholderRole}, - duplicateOccurrenceWarning{"b", "f", *scip.NewRange(placeholderRange), placeholderRole}, + duplicateOccurrenceWarning{"a", "f", scip.NewRangeUnchecked(placeholderRange), placeholderRole}, + duplicateOccurrenceWarning{"b", "f", scip.NewRangeUnchecked(placeholderRange), placeholderRole}, }, }, } diff --git a/go.mod b/go.mod index 1d9a9469..3f40f280 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/urfave/cli/v2 v2.25.7 golang.org/x/tools v0.12.0 google.golang.org/protobuf v1.31.0 + pgregory.net/rapid v1.1.0 ) require ( diff --git a/go.sum b/go.sum index a77e156e..39db4695 100644 --- a/go.sum +++ b/go.sum @@ -660,3 +660,5 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh mvdan.cc/gofumpt v0.4.0/go.mod h1:PljLOHDeZqgS8opHRKLzp2It2VBuSdteAgqUfzMTxlQ= mvdan.cc/gofumpt v0.5.0 h1:0EQ+Z56k8tXjj/6TQD25BFNKQXpCvT0rnansIc7Ug5E= mvdan.cc/gofumpt v0.5.0/go.mod h1:HBeVDtMKRZpXyxFciAirzdKklDlGu8aAy1wEbH5Y9js= +pgregory.net/rapid v1.1.0 h1:CMa0sjHSru3puNx+J0MIAuiiEV4N0qj8/cMWGBBCsjw= +pgregory.net/rapid v1.1.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= \ No newline at end of file