Skip to content

Commit efce0f5

Browse files
committed
gopls/internal/cache/metadata: assert graph is acyclic
This change causes the metadata graph to assert that it is acyclic before applying the updates. This should be an invariant, but the attached issues make us skeptical. Updates golang/go#64227 Updates golang/vscode-go#3126 Change-Id: I40b4fd06fcf2c64594b34b8c300f20ca0676d0fa Reviewed-on: https://go-review.googlesource.com/c/tools/+/560463 Reviewed-by: Robert Findley <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent c3f60b7 commit efce0f5

File tree

3 files changed

+65
-42
lines changed

3 files changed

+65
-42
lines changed

gopls/internal/cache/metadata/cycle_test.go

+9-42
Original file line numberDiff line numberDiff line change
@@ -8,58 +8,25 @@ import (
88
"sort"
99
"strings"
1010
"testing"
11+
12+
"golang.org/x/tools/gopls/internal/util/bug"
1113
)
1214

15+
func init() {
16+
bug.PanicOnBugs = true
17+
}
18+
1319
// This is an internal test of the breakImportCycles logic.
1420
func TestBreakImportCycles(t *testing.T) {
1521

16-
type Graph = map[PackageID]*Package
17-
18-
// cyclic returns a description of a cycle,
19-
// if the graph is cyclic, otherwise "".
20-
cyclic := func(graph Graph) string {
21-
const (
22-
unvisited = 0
23-
visited = 1
24-
onstack = 2
25-
)
26-
color := make(map[PackageID]int)
27-
var visit func(id PackageID) string
28-
visit = func(id PackageID) string {
29-
switch color[id] {
30-
case unvisited:
31-
color[id] = onstack
32-
case onstack:
33-
return string(id) // cycle!
34-
case visited:
35-
return ""
36-
}
37-
if mp := graph[id]; mp != nil {
38-
for _, depID := range mp.DepsByPkgPath {
39-
if cycle := visit(depID); cycle != "" {
40-
return string(id) + "->" + cycle
41-
}
42-
}
43-
}
44-
color[id] = visited
45-
return ""
46-
}
47-
for id := range graph {
48-
if cycle := visit(id); cycle != "" {
49-
return cycle
50-
}
51-
}
52-
return ""
53-
}
54-
5522
// parse parses an import dependency graph.
5623
// The input is a semicolon-separated list of node descriptions.
5724
// Each node description is a package ID, optionally followed by
5825
// "->" and a comma-separated list of successor IDs.
5926
// Thus "a->b;b->c,d;e" represents the set of nodes {a,b,e}
6027
// and the set of edges {a->b, b->c, b->d}.
61-
parse := func(s string) Graph {
62-
m := make(Graph)
28+
parse := func(s string) map[PackageID]*Package {
29+
m := make(map[PackageID]*Package)
6330
makeNode := func(name string) *Package {
6431
id := PackageID(name)
6532
n, ok := m[id]
@@ -98,7 +65,7 @@ func TestBreakImportCycles(t *testing.T) {
9865
// format formats an import graph, in lexicographic order,
9966
// in the notation of parse, but with a "!" after the name
10067
// of each node that has errors.
101-
format := func(graph Graph) string {
68+
format := func(graph map[PackageID]*Package) string {
10269
var items []string
10370
for _, mp := range graph {
10471
item := string(mp.ID)

gopls/internal/cache/metadata/graph.go

+53
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,22 @@ func (g *Graph) Update(updates map[PackageID]*Package) *Graph {
3838
return g
3939
}
4040

41+
// Debugging golang/go#64227, golang/vscode-go#3126:
42+
// Assert that the existing metadata graph is acyclic.
43+
if cycle := cyclic(g.Packages); cycle != "" {
44+
bug.Reportf("metadata is cyclic even before updates: %s", cycle)
45+
}
46+
// Assert that the updates contain no self-cycles.
47+
for id, mp := range updates {
48+
if mp != nil {
49+
for _, depID := range mp.DepsByPkgPath {
50+
if depID == id {
51+
bug.Reportf("self-cycle in metadata update: %s", id)
52+
}
53+
}
54+
}
55+
}
56+
4157
// Copy pkgs map then apply updates.
4258
pkgs := make(map[PackageID]*Package, len(g.Packages))
4359
for id, mp := range g.Packages {
@@ -223,6 +239,43 @@ func breakImportCycles(metadata, updates map[PackageID]*Package) {
223239
}
224240
}
225241

242+
// cyclic returns a description of a cycle,
243+
// if the graph is cyclic, otherwise "".
244+
func cyclic(graph map[PackageID]*Package) string {
245+
const (
246+
unvisited = 0
247+
visited = 1
248+
onstack = 2
249+
)
250+
color := make(map[PackageID]int)
251+
var visit func(id PackageID) string
252+
visit = func(id PackageID) string {
253+
switch color[id] {
254+
case unvisited:
255+
color[id] = onstack
256+
case onstack:
257+
return string(id) // cycle!
258+
case visited:
259+
return ""
260+
}
261+
if mp := graph[id]; mp != nil {
262+
for _, depID := range mp.DepsByPkgPath {
263+
if cycle := visit(depID); cycle != "" {
264+
return string(id) + "->" + cycle
265+
}
266+
}
267+
}
268+
color[id] = visited
269+
return ""
270+
}
271+
for id := range graph {
272+
if cycle := visit(id); cycle != "" {
273+
return cycle
274+
}
275+
}
276+
return ""
277+
}
278+
226279
// detectImportCycles reports cycles in the metadata graph. It returns a new
227280
// unordered array of all cycles (nontrivial strong components) in the
228281
// metadata graph reachable from a non-nil 'updates' value.

gopls/internal/util/bug/bug.go

+3
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ import (
2525
// PanicOnBugs controls whether to panic when bugs are reported.
2626
//
2727
// It may be set to true during testing.
28+
//
29+
// TODO(adonovan): should we make the default true, and
30+
// suppress it only in the product (gopls/main.go)?
2831
var PanicOnBugs = false
2932

3033
var (

0 commit comments

Comments
 (0)