Skip to content

Commit 4178e59

Browse files
varungandhi-srcChristoph Hegemann
andauthored
bindings/go: Add common APIs for Range and Position (#248)
- Adds error-checking to NewRange and returns an error for malformed ranges - Adds NewRangeUnchecked to handle construction without error-checking - Adds a bunch of helper functions for Range and Position for usage in Sourcegraph Co-authored-by: Christoph Hegemann <christoph.hegemann@sourcegraph.com>
1 parent b7127de commit 4178e59

File tree

11 files changed

+465
-110
lines changed

11 files changed

+465
-110
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,16 @@ SCIP schema:
66

77
- Added documentation that ranges must be half-open intervals.
88

9+
Go SCIP bindings:
10+
11+
- Breaking changes:
12+
- The `NewRange` function does well-formedness checks and returns `(Range, error)` instead of `*Range`.
13+
When skipping checks, `NewRangeUnchecked` can be used instead.
14+
- The `SortRanges` function takes a `[]Range` instead of a `[]*Range`
15+
to avoid extra heap allocations.
16+
- Features:
17+
- Added new methods for `Range` and `Position` types.
18+
919
## v0.3.3
1020

1121
SCIP schema:

bindings/go/scip/canonicalize.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ func RemoveIllegalOccurrences(occurrences []*Occurrence) []*Occurrence {
3636
// CanonicalizeOccurrence deterministically re-orders the fields of the given occurrence.
3737
func CanonicalizeOccurrence(occurrence *Occurrence) *Occurrence {
3838
// Express ranges as three-components if possible
39-
occurrence.Range = NewRange(occurrence.Range).SCIPRange()
39+
occurrence.Range = NewRangeUnchecked(occurrence.Range).SCIPRange()
4040
occurrence.Diagnostics = CanonicalizeDiagnostics(occurrence.Diagnostics)
4141
return occurrence
4242
}

bindings/go/scip/position.go

Lines changed: 158 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package scip
22

3-
// Range represents a range between two offset positions.
3+
import "fmt"
4+
5+
// Range represents [start, end) between two offset positions.
6+
//
47
// NOTE: the github.com/sourcegraph/sourcegraph/lib/codeintel/lsif/protocol package
58
// contains similarly shaped structs but this one exists primarily to make it
69
// easier to work with SCIP encoded positions, which have the type []int32
@@ -16,18 +19,109 @@ type Position struct {
1619
Character int32
1720
}
1821

19-
// NewRange converts an SCIP range into `Range`
20-
func NewRange(scipRange []int32) *Range {
21-
var endLine int32
22-
var endCharacter int32
23-
if len(scipRange) == 3 { // single line
24-
endLine = scipRange[0]
25-
endCharacter = scipRange[2]
26-
} else if len(scipRange) == 4 { // multi-line
22+
func (p Position) Compare(other Position) int {
23+
if p.Line < other.Line {
24+
return -1
25+
}
26+
if p.Line > other.Line {
27+
return 1
28+
}
29+
if p.Character < other.Character {
30+
return -1
31+
}
32+
if p.Character > other.Character {
33+
return 1
34+
}
35+
return 0
36+
}
37+
38+
func (p Position) Less(other Position) bool {
39+
if p.Line < other.Line {
40+
return true
41+
}
42+
if p.Line > other.Line {
43+
return false
44+
}
45+
return p.Character < other.Character
46+
}
47+
48+
//go:noinline
49+
func makeNewRangeError(startLine, endLine, startChar, endChar int32) (Range, error) {
50+
if startLine < 0 || endLine < 0 || startChar < 0 || endChar < 0 {
51+
return Range{}, NegativeOffsetsRangeError
52+
}
53+
if startLine > endLine || (startLine == endLine && startChar > endChar) {
54+
return Range{}, EndBeforeStartRangeError
55+
}
56+
panic("unreachable")
57+
}
58+
59+
// NewRange constructs a Range while checking if the input is valid.
60+
func NewRange(scipRange []int32) (Range, error) {
61+
// N.B. This function is kept small so that it can be inlined easily.
62+
// See also: https://github.com/golang/go/issues/17566
63+
var startLine, endLine, startChar, endChar int32
64+
switch len(scipRange) {
65+
case 3:
66+
startLine = scipRange[0]
67+
endLine = startLine
68+
startChar = scipRange[1]
69+
endChar = scipRange[2]
70+
if startLine >= 0 && startChar >= 0 && endChar >= startChar {
71+
break
72+
}
73+
return makeNewRangeError(startLine, endLine, startChar, endChar)
74+
case 4:
75+
startLine = scipRange[0]
76+
startChar = scipRange[1]
2777
endLine = scipRange[2]
78+
endChar = scipRange[3]
79+
if startLine >= 0 && startChar >= 0 &&
80+
((endLine > startLine && endChar >= 0) || (endLine == startLine && endChar >= startChar)) {
81+
break
82+
}
83+
return makeNewRangeError(startLine, endLine, startChar, endChar)
84+
default:
85+
return Range{}, IncorrectLengthRangeError
86+
}
87+
return Range{Start: Position{Line: startLine, Character: startChar}, End: Position{Line: endLine, Character: endChar}}, nil
88+
}
89+
90+
type RangeError int32
91+
92+
const (
93+
IncorrectLengthRangeError RangeError = iota
94+
NegativeOffsetsRangeError
95+
EndBeforeStartRangeError
96+
)
97+
98+
var _ error = RangeError(0)
99+
100+
func (e RangeError) Error() string {
101+
switch e {
102+
case IncorrectLengthRangeError:
103+
return "incorrect length"
104+
case NegativeOffsetsRangeError:
105+
return "negative offsets"
106+
case EndBeforeStartRangeError:
107+
return "end before start"
108+
}
109+
panic("unhandled range error")
110+
}
111+
112+
// NewRangeUnchecked converts an SCIP range into `Range`
113+
//
114+
// Pre-condition: The input slice must follow the SCIP range encoding.
115+
// https://sourcegraph.com/github.com/sourcegraph/scip/-/blob/scip.proto?L646:18-646:23
116+
func NewRangeUnchecked(scipRange []int32) Range {
117+
// Single-line case is most common
118+
endCharacter := scipRange[2]
119+
endLine := scipRange[0]
120+
if len(scipRange) == 4 { // multi-line
28121
endCharacter = scipRange[3]
122+
endLine = scipRange[2]
29123
}
30-
return &Range{
124+
return Range{
31125
Start: Position{
32126
Line: scipRange[0],
33127
Character: scipRange[1],
@@ -49,3 +143,57 @@ func (r Range) SCIPRange() []int32 {
49143
}
50144
return []int32{r.Start.Line, r.Start.Character, r.End.Line, r.End.Character}
51145
}
146+
147+
// Contains checks if position is within the range
148+
func (r Range) Contains(position Position) bool {
149+
return !position.Less(r.Start) && position.Less(r.End)
150+
}
151+
152+
// Intersects checks if two ranges intersect.
153+
//
154+
// case 1: r1.Start >= other.Start && r1.Start < other.End
155+
// case 2: r2.Start >= r1.Start && r2.Start < r1.End
156+
func (r Range) Intersects(other Range) bool {
157+
return r.Start.Less(other.End) && other.Start.Less(r.End)
158+
}
159+
160+
// Compare compares two ranges.
161+
//
162+
// Returns 0 if the ranges intersect (not just if they're equal).
163+
func (r Range) Compare(other Range) int {
164+
if r.Intersects(other) {
165+
return 0
166+
}
167+
return r.Start.Compare(other.Start)
168+
}
169+
170+
// Less compares two ranges, consistent with Compare.
171+
//
172+
// r.Compare(other) < 0 iff r.Less(other).
173+
func (r Range) Less(other Range) bool {
174+
return r.End.Compare(other.Start) <= 0
175+
}
176+
177+
// CompareStrict compares two ranges.
178+
//
179+
// Returns 0 iff the ranges are exactly equal.
180+
func (r Range) CompareStrict(other Range) int {
181+
if ret := r.Start.Compare(other.Start); ret != 0 {
182+
return ret
183+
}
184+
return r.End.Compare(other.End)
185+
}
186+
187+
// LessStrict compares two ranges, consistent with CompareStrict.
188+
//
189+
// r.CompareStrict(other) < 0 iff r.LessStrict(other).
190+
func (r Range) LessStrict(other Range) bool {
191+
if ret := r.Start.Compare(other.Start); ret != 0 {
192+
return ret < 0
193+
}
194+
return r.End.Less(other.End)
195+
}
196+
197+
func (r Range) String() string {
198+
return fmt.Sprintf("%d:%d-%d:%d", r.Start.Line, r.Start.Character, r.End.Line, r.End.Character)
199+
}

0 commit comments

Comments
 (0)