Skip to content

Commit

Permalink
bindings/go: Add common APIs for Range and Position (#248)
Browse files Browse the repository at this point in the history
- 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 <[email protected]>
  • Loading branch information
varungandhi-src and kritzcreek authored Jun 11, 2024
1 parent b7127de commit 4178e59
Show file tree
Hide file tree
Showing 11 changed files with 465 additions and 110 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion bindings/go/scip/canonicalize.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
168 changes: 158 additions & 10 deletions bindings/go/scip/position.go
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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],
Expand All @@ -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)
}
Loading

0 comments on commit 4178e59

Please sign in to comment.