Skip to content

Commit 45df7f6

Browse files
authored
go bindings: Remove occurrences with empty ranges during canonicalization (#180)
1 parent 3746944 commit 45df7f6

File tree

3 files changed

+241
-1
lines changed

3 files changed

+241
-1
lines changed

bindings/go/scip/canonicalize.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,29 @@ func CanonicalizeDocument(document *Document) *Document {
1010
// CanonicalizeOccurrences deterministically re-orders the fields of the given occurrence slice.
1111
func CanonicalizeOccurrences(occurrences []*Occurrence) []*Occurrence {
1212
canonicalized := make([]*Occurrence, 0, len(occurrences))
13-
for _, occurrence := range FlattenOccurrences(occurrences) {
13+
for _, occurrence := range FlattenOccurrences(RemoveIllegalOccurrences(occurrences)) {
1414
canonicalized = append(canonicalized, CanonicalizeOccurrence(occurrence))
1515
}
1616

1717
return SortOccurrences(canonicalized)
1818
}
1919

20+
// RemoveIllegalOccurrences removes all occurrences that do not include a range. This is
21+
// emitted by some indexers and will silently crash a downstream process, including further
22+
// canonicalization, when trying to convert an empty slice into a valid range.
23+
func RemoveIllegalOccurrences(occurrences []*Occurrence) []*Occurrence {
24+
filtered := occurrences[:0]
25+
for _, occurrence := range occurrences {
26+
if len(occurrence.Range) != 3 && len(occurrence.Range) != 4 {
27+
continue
28+
}
29+
30+
filtered = append(filtered, occurrence)
31+
}
32+
33+
return filtered
34+
}
35+
2036
// CanonicalizeOccurrence deterministically re-orders the fields of the given occurrence.
2137
func CanonicalizeOccurrence(occurrence *Occurrence) *Occurrence {
2238
// Express ranges as three-components if possible

bindings/go/scip/canonicalize_test.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package scip
2+
3+
import (
4+
"testing"
5+
6+
"github.com/google/go-cmp/cmp"
7+
)
8+
9+
func TestCanonicalizeDocument(t *testing.T) {
10+
document := &Document{
11+
RelativePath: "foo.go",
12+
Symbols: []*SymbolInformation{
13+
{Symbol: "foo"},
14+
{Symbol: "bar"},
15+
{Symbol: "bonk"},
16+
17+
// duplicates
18+
{Symbol: "baz", Relationships: []*Relationship{{Symbol: "bazzer", IsReference: true}}},
19+
{Symbol: "baz", Relationships: []*Relationship{{Symbol: "bazImpl", IsImplementation: true}}},
20+
21+
// duplicates
22+
{Symbol: "quux", Documentation: []string{"docs1"}},
23+
{Symbol: "quux", Documentation: []string{"docs2"}},
24+
},
25+
Occurrences: []*Occurrence{
26+
// illegal range (removed)
27+
{Range: []int32{}, Symbol: "foo"},
28+
29+
{Range: []int32{2, 1, 2, 4}, Symbol: "bar", SymbolRoles: int32(SymbolRole_Definition)},
30+
{Range: []int32{1, 1, 1, 4}, Symbol: "foo"},
31+
{Range: []int32{4, 1, 4, 4}, Symbol: "bonk"},
32+
{Range: []int32{6, 1, 6, 4}, Symbol: "honk"},
33+
34+
// duplicates (same symbol name)
35+
{Range: []int32{5, 1, 5, 4}, Symbol: "quux", OverrideDocumentation: []string{"primo"}},
36+
{Range: []int32{5, 1, 5, 4}, Symbol: "quux", OverrideDocumentation: []string{"secondo"}},
37+
38+
// duplicate (different symbol name)
39+
{Range: []int32{3, 1, 4, 4}, Symbol: "baz"},
40+
{Range: []int32{3, 1, 4, 4}, Symbol: "bazImpl"},
41+
},
42+
}
43+
canonicalizedDocument := CanonicalizeDocument(document)
44+
45+
expectedDocument := &Document{
46+
RelativePath: "foo.go",
47+
Symbols: []*SymbolInformation{
48+
{Symbol: "bar"},
49+
{Symbol: "baz", Relationships: []*Relationship{{Symbol: "bazImpl", IsImplementation: true}, {Symbol: "bazzer", IsReference: true}}},
50+
{Symbol: "bonk"},
51+
{Symbol: "foo"},
52+
{Symbol: "quux", Documentation: []string{"docs1", "docs2"}},
53+
},
54+
Occurrences: []*Occurrence{
55+
{Range: []int32{1, 1, 4}, Symbol: "foo"},
56+
{Range: []int32{2, 1, 4}, Symbol: "bar", SymbolRoles: int32(SymbolRole_Definition)},
57+
{Range: []int32{3, 1, 4, 4}, Symbol: "baz"},
58+
{Range: []int32{3, 1, 4, 4}, Symbol: "bazImpl"},
59+
{Range: []int32{4, 1, 4}, Symbol: "bonk"},
60+
{Range: []int32{5, 1, 4}, Symbol: "quux", OverrideDocumentation: []string{"primo", "secondo"}},
61+
{Range: []int32{6, 1, 4}, Symbol: "honk"},
62+
},
63+
}
64+
if diff := cmp.Diff(expectedDocument, canonicalizedDocument, compareDocuments); diff != "" {
65+
t.Fatalf("unexpected document (-want +got):\n%s", diff)
66+
}
67+
}

bindings/go/scip/flatten_test.go

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
package scip
2+
3+
import (
4+
"testing"
5+
6+
"github.com/google/go-cmp/cmp"
7+
)
8+
9+
func TestFlattenDocuments(t *testing.T) {
10+
s1 := []*SymbolInformation{{Symbol: "foo"}, {Symbol: "bar"}}
11+
o1 := []*Occurrence{{Range: []int32{1, 2, 3}, Symbol: "foo"}, {Range: []int32{2, 3, 4}, Symbol: "bar"}}
12+
s2 := []*SymbolInformation{{Symbol: "baz"}, {Symbol: "bonk"}}
13+
o2 := []*Occurrence{{Range: []int32{3, 4, 5}, Symbol: "baz"}, {Range: []int32{4, 5, 6}, Symbol: "bonk"}}
14+
s3 := []*SymbolInformation{{Symbol: "quux"}}
15+
o3 := []*Occurrence{{Range: []int32{5, 6, 7}, Symbol: "quux"}}
16+
17+
documents := []*Document{
18+
{RelativePath: "foo.go", Symbols: s1, Occurrences: o1},
19+
{RelativePath: "bar.go", Symbols: s2, Occurrences: o2},
20+
{RelativePath: "foo.go", Symbols: s3, Occurrences: o3},
21+
}
22+
flattenedDocuments := SortDocuments(FlattenDocuments(documents))
23+
24+
expectedDocuments := []*Document{
25+
{RelativePath: "bar.go", Symbols: s2, Occurrences: o2},
26+
{RelativePath: "foo.go", Symbols: append(s1, s3...), Occurrences: append(o1, o3...)},
27+
}
28+
if diff := cmp.Diff(expectedDocuments, flattenedDocuments, compareDocuments); diff != "" {
29+
t.Fatalf("unexpected documents (-want +got):\n%s", diff)
30+
}
31+
}
32+
33+
var compareDocuments = cmp.Comparer(func(a, b *Document) bool {
34+
if a.Language != b.Language || a.RelativePath != b.RelativePath || a.Text != b.Text {
35+
return false
36+
}
37+
if !compareSlices(a.Symbols, b.Symbols, compareSymbol) {
38+
return false
39+
}
40+
if !compareSlices(a.Occurrences, b.Occurrences, compareOccurrence) {
41+
return false
42+
}
43+
44+
return true
45+
})
46+
47+
func compareSlices[T any](as, bs []T, f func(a, b T) bool) bool {
48+
if len(as) != len(bs) {
49+
return false
50+
}
51+
52+
for i, a := range as {
53+
if !f(a, bs[i]) {
54+
return false
55+
}
56+
}
57+
58+
return true
59+
}
60+
61+
func compareSymbol(a, b *SymbolInformation) bool {
62+
if a.Symbol != b.Symbol {
63+
return false
64+
}
65+
if a.Kind != b.Kind {
66+
return false
67+
}
68+
if a.DisplayName != b.DisplayName {
69+
return false
70+
}
71+
if a.SignatureDocumentation != b.SignatureDocumentation {
72+
return false
73+
}
74+
if a.EnclosingSymbol != b.EnclosingSymbol {
75+
return false
76+
}
77+
78+
if !compareSlices(a.Documentation, b.Documentation, compareValue[string]) {
79+
return false
80+
}
81+
if !compareSlices(a.Relationships, b.Relationships, compareRelationship) {
82+
return false
83+
}
84+
85+
return true
86+
}
87+
88+
func compareOccurrence(a, b *Occurrence) bool {
89+
if a.Symbol != b.Symbol {
90+
return false
91+
}
92+
if a.SymbolRoles != b.SymbolRoles {
93+
return false
94+
}
95+
if a.SyntaxKind != b.SyntaxKind {
96+
return false
97+
}
98+
99+
if !compareSlices(a.Range, b.Range, compareValue[int32]) {
100+
return false
101+
}
102+
if !compareSlices(a.OverrideDocumentation, b.OverrideDocumentation, compareValue[string]) {
103+
return false
104+
}
105+
if !compareSlices(a.Diagnostics, b.Diagnostics, compareDiagnostic) {
106+
return false
107+
}
108+
if !compareSlices(a.EnclosingRange, b.EnclosingRange, compareValue[int32]) {
109+
return false
110+
}
111+
112+
return true
113+
}
114+
115+
func compareRelationship(a, b *Relationship) bool {
116+
if a.Symbol != b.Symbol {
117+
return false
118+
}
119+
if a.IsReference != b.IsReference {
120+
return false
121+
}
122+
if a.IsImplementation != b.IsImplementation {
123+
return false
124+
}
125+
if a.IsTypeDefinition != b.IsTypeDefinition {
126+
return false
127+
}
128+
if a.IsDefinition != b.IsDefinition {
129+
return false
130+
}
131+
132+
return true
133+
}
134+
135+
func compareDiagnostic(a, b *Diagnostic) bool {
136+
if a.Severity != b.Severity {
137+
return false
138+
}
139+
if a.Code != b.Code {
140+
return false
141+
}
142+
if a.Message != b.Message {
143+
return false
144+
}
145+
if a.Source != b.Source {
146+
return false
147+
}
148+
if !compareSlices(a.Tags, b.Tags, compareValue[DiagnosticTag]) {
149+
return false
150+
}
151+
152+
return true
153+
}
154+
155+
func compareValue[T comparable](a, b T) bool {
156+
return a == b
157+
}

0 commit comments

Comments
 (0)