Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions bindings/go/scip/canonicalize.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ package scip
// CanonicalizeDocument deterministically sorts and merges fields of the given document.
//
// Post-conditions:
// 1. The Occurrences field only contains those with well-formed ranges
// (length 3 or 4, potentially empty).
// 1. The Occurrences field only contains those with well-formed ranges.
// 2. The Occurrences field is sorted in ascending order of ranges based on
// Range.CompareStrict
// 3. The Symbols field is sorted in ascending order based on the symbol name,
Expand All @@ -31,7 +30,7 @@ func CanonicalizeOccurrences(occurrences []*Occurrence) []*Occurrence {
func RemoveIllegalOccurrences(occurrences []*Occurrence) []*Occurrence {
filtered := occurrences[:0]
for _, occurrence := range occurrences {
if len(occurrence.Range) != 3 && len(occurrence.Range) != 4 {
if _, ok := occurrence.SourceRange(); !ok {
continue
}

Expand All @@ -42,9 +41,16 @@ func RemoveIllegalOccurrences(occurrences []*Occurrence) []*Occurrence {
}

// CanonicalizeOccurrence deterministically re-orders the fields of the given occurrence.
//
// It normalizes the occurrence's range encoding so that occurrences carrying
// only the deprecated `repeated int32 range` field are re-emitted with the
// most compact three-element form. Occurrences that carry a typed range are
// left unchanged.
func CanonicalizeOccurrence(occurrence *Occurrence) *Occurrence {
// Express ranges as three-components if possible
occurrence.Range = NewRangeUnchecked(occurrence.Range).SCIPRange()
if occurrence.GetTypedRange() == nil && len(occurrence.Range) > 0 {
// Express ranges as three-components if possible
occurrence.Range = NewRangeUnchecked(occurrence.Range).SCIPRange()
}
occurrence.Diagnostics = CanonicalizeDiagnostics(occurrence.Diagnostics)
return occurrence
}
Expand Down
6 changes: 1 addition & 5 deletions bindings/go/scip/flatten.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,7 @@ func FlattenOccurrences(occurrences []*Occurrence) []*Occurrence {
for _, occurrence := range occurrences[1:] {
top := flattened[len(flattened)-1]

if !rawRangesEqual(top.Range, occurrence.Range) {
flattened = append(flattened, occurrence)
continue
}
if top.Symbol != occurrence.Symbol {
if top.Compare(occurrence) != 0 {
flattened = append(flattened, occurrence)
continue
}
Expand Down
129 changes: 129 additions & 0 deletions bindings/go/scip/occurrence_range.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package scip

import "strings"

// SourceRange returns the source range of this occurrence and whether one is
// set. `typed_range` takes precedence over the deprecated `range` field.
// Malformed deprecated ranges (length != 3 or 4) are reported as missing; use
// `scip lint` to surface them.
func (occ *Occurrence) SourceRange() (Range, bool) {
if r, ok := readTypedRange(occ.GetTypedRange()); ok {
return r, true
}
if len(occ.Range) != 3 && len(occ.Range) != 4 {
return Range{}, false
}
return NewRangeUnchecked(occ.Range), true
}

// EnclosingSourceRange returns the enclosing source range of this occurrence
// and whether one is set. `typed_enclosing_range` takes precedence over the
// deprecated `enclosing_range` field.
func (occ *Occurrence) EnclosingSourceRange() (Range, bool) {
if r, ok := readTypedRange(occ.GetTypedEnclosingRange()); ok {
return r, true
}
if len(occ.EnclosingRange) != 3 && len(occ.EnclosingRange) != 4 {
return Range{}, false
}
return NewRangeUnchecked(occ.EnclosingRange), true
}

// AsTypedRange returns this range as the appropriate Occurrence.TypedRange
// oneof variant (SingleLineRange when r fits on one line, MultiLineRange
// otherwise). Useful for setting the source range directly in an Occurrence
// struct literal:
//
// &Occurrence{TypedRange: r.AsTypedRange(), ...}
func (r Range) AsTypedRange() isOccurrence_TypedRange {
if r.IsSingleLine() {
return &Occurrence_SingleLineRange{SingleLineRange: r.ToSingleLineRange()}
}
return &Occurrence_MultiLineRange{MultiLineRange: r.ToMultiLineRange()}
}

// AsTypedEnclosingRange is the counterpart of AsTypedRange for the
// `typed_enclosing_range` oneof.
func (r Range) AsTypedEnclosingRange() isOccurrence_TypedEnclosingRange {
if r.IsSingleLine() {
return &Occurrence_SingleLineEnclosingRange{SingleLineEnclosingRange: r.ToSingleLineRange()}
}
return &Occurrence_MultiLineEnclosingRange{MultiLineEnclosingRange: r.ToMultiLineRange()}
}

// SetSourceRange sets the source range using the typed `typed_range` encoding
// and clears the deprecated `range` field.
func (occ *Occurrence) SetSourceRange(r Range) {
occ.Range = nil
occ.TypedRange = r.AsTypedRange()
}

// SetEnclosingSourceRange is the counterpart of SetSourceRange for the
// enclosing range.
func (occ *Occurrence) SetEnclosingSourceRange(r Range) {
occ.EnclosingRange = nil
occ.TypedEnclosingRange = r.AsTypedEnclosingRange()
}

// Compare orders occurrences in the canonical SCIP ordering: ascending by
// source range, with symbol name as a tiebreaker. Returns -1, 0, or +1.
//
// Occurrences missing a source range compare as if their range were the
// zero range; such occurrences are illegal per the SCIP spec and should be
// surfaced via `scip lint`.
func (occ *Occurrence) Compare(other *Occurrence) int {
r1, _ := occ.SourceRange()
r2, _ := other.SourceRange()
if c := r1.CompareStrict(r2); c != 0 {
return c
}
return strings.Compare(occ.Symbol, other.Symbol)
}

// Contains reports whether the source range of this occurrence contains the
// given position. Returns false if the occurrence has no range.
func (occ *Occurrence) Contains(pos Position) bool {
r, ok := occ.SourceRange()
return ok && r.Contains(pos)
}

// ToRange returns this single-line range as a Range.
func (sr *SingleLineRange) ToRange() Range {
return Range{
Start: Position{Line: sr.Line, Character: sr.StartCharacter},
End: Position{Line: sr.Line, Character: sr.EndCharacter},
}
}

// ToRange returns this multi-line range as a Range.
func (mr *MultiLineRange) ToRange() Range {
return Range{
Start: Position{Line: mr.StartLine, Character: mr.StartCharacter},
End: Position{Line: mr.EndLine, Character: mr.EndCharacter},
}
}

// readTypedRange decodes any of the four typed-range oneof variants on
// Occurrence into a Range. Returns (Range{}, false) when typed is nil or holds
// a nil message pointer.
func readTypedRange(typed any) (Range, bool) {
switch tr := typed.(type) {
case *Occurrence_SingleLineRange:
if tr.SingleLineRange != nil {
return tr.SingleLineRange.ToRange(), true
}
case *Occurrence_MultiLineRange:
if tr.MultiLineRange != nil {
return tr.MultiLineRange.ToRange(), true
}
case *Occurrence_SingleLineEnclosingRange:
if tr.SingleLineEnclosingRange != nil {
return tr.SingleLineEnclosingRange.ToRange(), true
}
case *Occurrence_MultiLineEnclosingRange:
if tr.MultiLineEnclosingRange != nil {
return tr.MultiLineEnclosingRange.ToRange(), true
}
}
return Range{}, false
}
194 changes: 194 additions & 0 deletions bindings/go/scip/occurrence_range_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
package scip

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestOccurrence_SourceRange_TypedSingleLine(t *testing.T) {
occ := &Occurrence{
TypedRange: &Occurrence_SingleLineRange{
SingleLineRange: &SingleLineRange{Line: 5, StartCharacter: 2, EndCharacter: 7},
},
}
r, ok := occ.SourceRange()
require.True(t, ok)
require.Equal(t, Range{Start: Position{5, 2}, End: Position{5, 7}}, r)
}

func TestOccurrence_SourceRange_TypedMultiLine(t *testing.T) {
occ := &Occurrence{
TypedRange: &Occurrence_MultiLineRange{
MultiLineRange: &MultiLineRange{StartLine: 1, StartCharacter: 2, EndLine: 3, EndCharacter: 4},
},
}
r, ok := occ.SourceRange()
require.True(t, ok)
require.Equal(t, Range{Start: Position{1, 2}, End: Position{3, 4}}, r)
}

func TestOccurrence_SourceRange_DeprecatedFallback(t *testing.T) {
occ := &Occurrence{Range: []int32{2, 3, 5}}
r, ok := occ.SourceRange()
require.True(t, ok)
require.Equal(t, Range{Start: Position{2, 3}, End: Position{2, 5}}, r)
}

func TestOccurrence_SourceRange_TypedTakesPrecedenceOverDeprecated(t *testing.T) {
// Per scip.proto, when both encodings are present the typed form wins.
occ := &Occurrence{
Range: []int32{100, 100, 100}, // deliberately disagrees
TypedRange: &Occurrence_SingleLineRange{
SingleLineRange: &SingleLineRange{Line: 5, StartCharacter: 2, EndCharacter: 7},
},
}
r, ok := occ.SourceRange()
require.True(t, ok)
require.Equal(t, Range{Start: Position{5, 2}, End: Position{5, 7}}, r)
}

func TestOccurrence_SourceRange_Missing(t *testing.T) {
occ := &Occurrence{}
r, ok := occ.SourceRange()
require.False(t, ok)
require.Equal(t, Range{}, r)
}

func TestOccurrence_SourceRange_DeprecatedMalformed(t *testing.T) {
occ := &Occurrence{Range: []int32{1, 2}} // wrong length
r, ok := occ.SourceRange()
require.False(t, ok)
require.Equal(t, Range{}, r)
}

func TestOccurrence_SourceRange_NilTypedInnerFallsBack(t *testing.T) {
occ := &Occurrence{
Range: []int32{2, 3, 5},
TypedRange: &Occurrence_SingleLineRange{SingleLineRange: nil},
}
r, ok := occ.SourceRange()
require.True(t, ok)
require.Equal(t, Range{Start: Position{2, 3}, End: Position{2, 5}}, r)
}

func TestOccurrence_SourceRange_NilTypedInnerNoFallback(t *testing.T) {
occ := &Occurrence{
TypedRange: &Occurrence_MultiLineRange{MultiLineRange: nil},
}
r, ok := occ.SourceRange()
require.False(t, ok)
require.Equal(t, Range{}, r)
}

func TestOccurrence_EnclosingSourceRange_TypedSingleLine(t *testing.T) {
occ := &Occurrence{
TypedEnclosingRange: &Occurrence_SingleLineEnclosingRange{
SingleLineEnclosingRange: &SingleLineRange{Line: 5, StartCharacter: 0, EndCharacter: 10},
},
}
r, ok := occ.EnclosingSourceRange()
require.True(t, ok)
require.Equal(t, Range{Start: Position{5, 0}, End: Position{5, 10}}, r)
}

func TestOccurrence_EnclosingSourceRange_TypedMultiLine(t *testing.T) {
occ := &Occurrence{
TypedEnclosingRange: &Occurrence_MultiLineEnclosingRange{
MultiLineEnclosingRange: &MultiLineRange{StartLine: 1, StartCharacter: 0, EndLine: 9, EndCharacter: 1},
},
}
r, ok := occ.EnclosingSourceRange()
require.True(t, ok)
require.Equal(t, Range{Start: Position{1, 0}, End: Position{9, 1}}, r)
}

func TestOccurrence_EnclosingSourceRange_DeprecatedFallback(t *testing.T) {
occ := &Occurrence{EnclosingRange: []int32{2, 0, 5, 1}}
r, ok := occ.EnclosingSourceRange()
require.True(t, ok)
require.Equal(t, Range{Start: Position{2, 0}, End: Position{5, 1}}, r)
}

func TestOccurrence_EnclosingSourceRange_TypedTakesPrecedence(t *testing.T) {
occ := &Occurrence{
EnclosingRange: []int32{100, 100, 100},
TypedEnclosingRange: &Occurrence_SingleLineEnclosingRange{
SingleLineEnclosingRange: &SingleLineRange{Line: 5, StartCharacter: 0, EndCharacter: 10},
},
}
r, ok := occ.EnclosingSourceRange()
require.True(t, ok)
require.Equal(t, Range{Start: Position{5, 0}, End: Position{5, 10}}, r)
}

func TestOccurrence_EnclosingSourceRange_Missing(t *testing.T) {
occ := &Occurrence{}
r, ok := occ.EnclosingSourceRange()
require.False(t, ok)
require.Equal(t, Range{}, r)
}

func TestOccurrence_SetSourceRange_SingleLine(t *testing.T) {
occ := &Occurrence{Range: []int32{99, 99, 99}}
occ.SetSourceRange(Range{Start: Position{3, 1}, End: Position{3, 8}})
require.Nil(t, occ.Range)
tr, ok := occ.TypedRange.(*Occurrence_SingleLineRange)
require.True(t, ok)
require.Equal(t, &SingleLineRange{Line: 3, StartCharacter: 1, EndCharacter: 8}, tr.SingleLineRange)

r, ok := occ.SourceRange()
require.True(t, ok)
require.Equal(t, Range{Start: Position{3, 1}, End: Position{3, 8}}, r)
}

func TestOccurrence_SetSourceRange_MultiLine(t *testing.T) {
occ := &Occurrence{}
occ.SetSourceRange(Range{Start: Position{1, 0}, End: Position{4, 2}})
tr, ok := occ.TypedRange.(*Occurrence_MultiLineRange)
require.True(t, ok)
require.Equal(t, &MultiLineRange{StartLine: 1, StartCharacter: 0, EndLine: 4, EndCharacter: 2}, tr.MultiLineRange)
}

func TestOccurrence_SetEnclosingSourceRange(t *testing.T) {
occ := &Occurrence{EnclosingRange: []int32{99, 99, 99}}
occ.SetEnclosingSourceRange(Range{Start: Position{1, 0}, End: Position{5, 1}})
require.Nil(t, occ.EnclosingRange)
tr, ok := occ.TypedEnclosingRange.(*Occurrence_MultiLineEnclosingRange)
require.True(t, ok)
require.Equal(t, &MultiLineRange{StartLine: 1, StartCharacter: 0, EndLine: 5, EndCharacter: 1}, tr.MultiLineEnclosingRange)
}

func TestOccurrence_Compare(t *testing.T) {
mkOcc := func(r []int32, sym string) *Occurrence {
return &Occurrence{Range: r, Symbol: sym}
}
tests := []struct {
name string
a, b *Occurrence
want int
}{
{"equal", mkOcc([]int32{1, 0, 5}, "x"), mkOcc([]int32{1, 0, 5}, "x"), 0},
{"earlier range", mkOcc([]int32{0, 0, 5}, "x"), mkOcc([]int32{1, 0, 5}, "x"), -1},
{"later range", mkOcc([]int32{2, 0, 5}, "x"), mkOcc([]int32{1, 0, 5}, "x"), 1},
{"same range, earlier symbol", mkOcc([]int32{1, 0, 5}, "a"), mkOcc([]int32{1, 0, 5}, "b"), -1},
{"same range, later symbol", mkOcc([]int32{1, 0, 5}, "b"), mkOcc([]int32{1, 0, 5}, "a"), 1},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
require.Equal(t, tc.want, tc.a.Compare(tc.b))
})
}
}

func TestOccurrence_Contains(t *testing.T) {
occ := &Occurrence{Range: []int32{2, 5, 10}}
require.True(t, occ.Contains(Position{2, 5}))
require.True(t, occ.Contains(Position{2, 9}))
require.False(t, occ.Contains(Position{2, 4}))
require.False(t, occ.Contains(Position{2, 10}))
require.False(t, occ.Contains(Position{3, 0}))

empty := &Occurrence{}
require.False(t, empty.Contains(Position{0, 0}))
}
4 changes: 2 additions & 2 deletions bindings/go/scip/parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ func TestEmpty(t *testing.T) {
}

func TestFuzz(t *testing.T) {
pat := regexp.MustCompile("^(state|sizeCache|unknownFields|SignatureDocumentation)$")
pat := regexp.MustCompile("^(state|sizeCache|unknownFields|SignatureDocumentation|TypedRange|TypedEnclosingRange)$")
f := fuzz.New().NumElements(0, 2).SkipFieldsWithPattern(pat)
for i := 0; i < 100; i++ {
index := Index{}
Expand Down Expand Up @@ -72,7 +72,7 @@ func TestLargeDocuments(t *testing.T) {
}

func TestDocumentsOnly(t *testing.T) {
pat := regexp.MustCompile("^(state|sizeCache|unknownFields|SignatureDocumentation)$")
pat := regexp.MustCompile("^(state|sizeCache|unknownFields|SignatureDocumentation|TypedRange|TypedEnclosingRange)$")
f := fuzz.New().NumElements(0, 2).SkipFieldsWithPattern(pat)
for i := 0; i < 100; i++ {
index := Index{}
Expand Down
Loading
Loading