Skip to content

Commit affb5fc

Browse files
committed
gopls/internal/lsp/source: fix crash in definitions of builtins
Definition queries on built-ins should report the pseudo-package "builtin", and queries on unsafe.Pointer et al should report the real (if irregular) package "unsafe". This CL fixes a bug wherein the latter was crashing. The solution required synthesizing Metadata("unsafe").GoFiles=["unsafe/unsafe.go"] from the filename in builtins, because unfortunately go/packages discards it. Also, references queries of builtins and unsafe.Pointer should be rejected with a clear error. The symbol tests were tweaked to handle the new matches in the unsafe package. Better ideas welcome. Fixes golang/go#43058 Change-Id: I9d7cd7ffcb679d77f9859e39b240a8eb605a267b Reviewed-on: https://go-review.googlesource.com/c/tools/+/491036 TryBot-Result: Gopher Robot <[email protected]> Run-TryBot: Alan Donovan <[email protected]> Reviewed-by: Robert Findley <[email protected]>
1 parent 558d701 commit affb5fc

File tree

9 files changed

+183
-102
lines changed

9 files changed

+183
-102
lines changed

gopls/internal/lsp/cache/load.go

+26
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,31 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...loadSc
144144
return fmt.Errorf("packages.Load error: %w", err)
145145
}
146146

147+
// Workaround for a bug (?) that has been in go/packages since
148+
// the outset: Package("unsafe").GoFiles=[], whereas it should
149+
// include unsafe/unsafe.go. Derive it from builtins.go.
150+
//
151+
// This workaround relies on the fact that we always add both
152+
// builtins and unsafe to the set of scopes in the workspace load.
153+
//
154+
// TODO(adonovan): fix upstream in go/packages.
155+
// (Does this need a proposal? Arguably not.)
156+
{
157+
var builtin, unsafe *packages.Package
158+
for _, pkg := range pkgs {
159+
if pkg.ID == "unsafe" {
160+
unsafe = pkg
161+
} else if pkg.ID == "builtin" {
162+
builtin = pkg
163+
}
164+
}
165+
if builtin != nil && unsafe != nil && len(builtin.GoFiles) == 1 {
166+
unsafe.GoFiles = []string{
167+
filepath.Join(filepath.Dir(builtin.GoFiles[0]), "../unsafe/unsafe.go"),
168+
}
169+
}
170+
}
171+
147172
moduleErrs := make(map[string][]packages.Error) // module path -> errors
148173
filterFunc := s.view.filterFunc()
149174
newMetadata := make(map[PackageID]*source.Metadata)
@@ -453,6 +478,7 @@ func buildMetadata(ctx context.Context, pkg *packages.Package, cfg *packages.Con
453478
//
454479
// We should consider doing a more complete guard against import cycles
455480
// elsewhere.
481+
// [Update: we do! breakImportCycles in metadataGraph.Clone. Delete this.]
456482
for _, prev := range path {
457483
if prev == id {
458484
return fmt.Errorf("import cycle detected: %q", id)

gopls/internal/lsp/cache/view.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -741,8 +741,10 @@ func (s *snapshot) loadWorkspace(ctx context.Context, firstAttempt bool) (loadEr
741741
// If we're loading anything, ensure we also load builtin,
742742
// since it provides fake definitions (and documentation)
743743
// for types like int that are used everywhere.
744+
// ("unsafe" is also needed since its sole GoFiles is
745+
// derived from that of "builtin" via a workaround in load.)
744746
if len(scopes) > 0 {
745-
scopes = append(scopes, packageLoadScope("builtin"))
747+
scopes = append(scopes, packageLoadScope("builtin"), packageLoadScope("unsafe"))
746748
}
747749
loadErr = s.load(ctx, true, scopes...)
748750

gopls/internal/lsp/source/definition.go

+39-20
Original file line numberDiff line numberDiff line change
@@ -64,42 +64,61 @@ func Definition(ctx context.Context, snapshot Snapshot, fh FileHandle, position
6464
return nil, nil
6565
}
6666

67-
// Handle built-in identifiers.
68-
if obj.Parent() == types.Universe {
69-
builtin, err := snapshot.BuiltinFile(ctx)
70-
if err != nil {
71-
return nil, err
67+
// Handle objects with no position: builtin, unsafe.
68+
if !obj.Pos().IsValid() {
69+
var pgf *ParsedGoFile
70+
if obj.Parent() == types.Universe {
71+
// pseudo-package "builtin"
72+
builtinPGF, err := snapshot.BuiltinFile(ctx)
73+
if err != nil {
74+
return nil, err
75+
}
76+
pgf = builtinPGF
77+
78+
} else if obj.Pkg() == types.Unsafe {
79+
// package "unsafe"
80+
unsafe := snapshot.Metadata("unsafe")
81+
if unsafe == nil {
82+
return nil, fmt.Errorf("no metadata for package 'unsafe'")
83+
}
84+
uri := unsafe.GoFiles[0]
85+
fh, err := snapshot.ReadFile(ctx, uri)
86+
if err != nil {
87+
return nil, err
88+
}
89+
pgf, err = snapshot.ParseGo(ctx, fh, ParseFull&^SkipObjectResolution)
90+
if err != nil {
91+
return nil, err
92+
}
93+
94+
} else {
95+
return nil, bug.Errorf("internal error: no position for %v", obj.Name())
7296
}
73-
// Note that builtinObj is an ast.Object, not types.Object :)
74-
builtinObj := builtin.File.Scope.Lookup(obj.Name())
75-
if builtinObj == nil {
76-
// Every builtin should have documentation.
77-
return nil, bug.Errorf("internal error: no builtin object for %s", obj.Name())
97+
// Inv: pgf ∈ {builtin,unsafe}.go
98+
99+
// Use legacy (go/ast) object resolution.
100+
astObj := pgf.File.Scope.Lookup(obj.Name())
101+
if astObj == nil {
102+
// Every built-in should have documentation syntax.
103+
return nil, bug.Errorf("internal error: no object for %s", obj.Name())
78104
}
79-
decl, ok := builtinObj.Decl.(ast.Node)
105+
decl, ok := astObj.Decl.(ast.Node)
80106
if !ok {
81107
return nil, bug.Errorf("internal error: no declaration for %s", obj.Name())
82108
}
83-
// The builtin package isn't in the dependency graph, so the usual
84-
// utilities won't work here.
85-
loc, err := builtin.PosLocation(decl.Pos(), decl.Pos()+token.Pos(len(obj.Name())))
109+
loc, err := pgf.PosLocation(decl.Pos(), decl.Pos()+token.Pos(len(obj.Name())))
86110
if err != nil {
87111
return nil, err
88112
}
89113
return []protocol.Location{loc}, nil
90114
}
91115

92116
// Finally, map the object position.
93-
var locs []protocol.Location
94-
if !obj.Pos().IsValid() {
95-
return nil, bug.Errorf("internal error: no position for %v", obj.Name())
96-
}
97117
loc, err := mapPosition(ctx, pkg.FileSet(), snapshot, obj.Pos(), adjustedObjEnd(obj))
98118
if err != nil {
99119
return nil, err
100120
}
101-
locs = append(locs, loc)
102-
return locs, nil
121+
return []protocol.Location{loc}, nil
103122
}
104123

105124
// referencedObject returns the identifier and object referenced at the

gopls/internal/lsp/source/hover.go

-2
Original file line numberDiff line numberDiff line change
@@ -364,8 +364,6 @@ func hoverBuiltin(ctx context.Context, snapshot Snapshot, obj types.Object) (*Ho
364364
return nil, err
365365
}
366366

367-
// TODO(rfindley): add a test for jump to definition of error.Error (which is
368-
// probably failing, considering it lacks special handling).
369367
if obj.Name() == "Error" {
370368
signature := obj.String()
371369
return &HoverJSON{

gopls/internal/lsp/source/references.go

+6-6
Original file line numberDiff line numberDiff line change
@@ -246,14 +246,14 @@ func ordinaryReferences(ctx context.Context, snapshot Snapshot, uri span.URI, pp
246246

247247
// nil, error, error.Error, iota, or other built-in?
248248
if obj.Pkg() == nil {
249-
// For some reason, existing tests require that iota has no references,
250-
// nor an error. TODO(adonovan): do something more principled.
251-
if obj.Name() == "iota" {
252-
return nil, nil
253-
}
254-
255249
return nil, fmt.Errorf("references to builtin %q are not supported", obj.Name())
256250
}
251+
if !obj.Pos().IsValid() {
252+
if obj.Pkg().Path() != "unsafe" {
253+
bug.Reportf("references: object %v has no position", obj)
254+
}
255+
return nil, fmt.Errorf("references to unsafe.%s are not supported", obj.Name())
256+
}
257257

258258
// Find metadata of all packages containing the object's defining file.
259259
// This may include the query pkg, and possibly other variants.

gopls/internal/regtest/marker/testdata/references/iota.txt

-16
This file was deleted.

gopls/internal/regtest/misc/references_test.go

+54
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,60 @@ func _() {
100100
})
101101
}
102102

103+
func TestDefsRefsBuiltins(t *testing.T) {
104+
testenv.NeedsGo1Point(t, 17) // for unsafe.{Add,Slice}
105+
// TODO(adonovan): add unsafe.SliceData,String,StringData} in later go versions.
106+
const files = `
107+
-- go.mod --
108+
module example.com
109+
go 1.16
110+
111+
-- a.go --
112+
package a
113+
114+
import "unsafe"
115+
116+
const _ = iota
117+
var _ error
118+
var _ int
119+
var _ = append()
120+
var _ = unsafe.Pointer(nil)
121+
var _ = unsafe.Add(nil, nil)
122+
var _ = unsafe.Sizeof(0)
123+
var _ = unsafe.Alignof(0)
124+
var _ = unsafe.Slice(nil, 0)
125+
`
126+
127+
Run(t, files, func(t *testing.T, env *Env) {
128+
env.OpenFile("a.go")
129+
for _, name := range strings.Fields(
130+
"iota error int nil append iota Pointer Sizeof Alignof Add Slice") {
131+
loc := env.RegexpSearch("a.go", `\b`+name+`\b`)
132+
133+
// definition -> {builtin,unsafe}.go
134+
def, err := env.Editor.GoToDefinition(env.Ctx, loc)
135+
if err != nil {
136+
t.Errorf("definition(%q) failed: %v", name, err)
137+
} else if (!strings.HasSuffix(string(def.URI), "builtin.go") &&
138+
!strings.HasSuffix(string(def.URI), "unsafe.go")) ||
139+
def.Range.Start.Line == 0 {
140+
t.Errorf("definition(%q) = %v, want {builtin,unsafe}.go",
141+
name, def)
142+
}
143+
144+
// "references to (builtin "Foo"|unsafe.Foo) are not supported"
145+
_, err = env.Editor.References(env.Ctx, loc)
146+
gotErr := fmt.Sprint(err)
147+
if !strings.Contains(gotErr, "references to") ||
148+
!strings.Contains(gotErr, "not supported") ||
149+
!strings.Contains(gotErr, name) {
150+
t.Errorf("references(%q) error: got %q, want %q",
151+
name, gotErr, "references to ... are not supported")
152+
}
153+
}
154+
})
155+
}
156+
103157
func TestPackageReferences(t *testing.T) {
104158
tests := []struct {
105159
packageName string

gopls/internal/regtest/misc/workspace_symbol_test.go

+22-29
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ package misc
77
import (
88
"testing"
99

10-
"golang.org/x/tools/gopls/internal/lsp/protocol"
10+
"github.com/google/go-cmp/cmp"
1111
. "golang.org/x/tools/gopls/internal/lsp/regtest"
1212
"golang.org/x/tools/gopls/internal/lsp/source"
1313
)
@@ -21,31 +21,27 @@ go 1.17
2121
-- a.go --
2222
package p
2323
24-
const C1 = "a.go"
24+
const K1 = "a.go"
2525
-- exclude.go --
2626
2727
//go:build exclude
2828
// +build exclude
2929
3030
package exclude
3131
32-
const C2 = "exclude.go"
32+
const K2 = "exclude.go"
3333
`
3434

35+
// NB: the name K was chosen to avoid spurious
36+
// matches in the always-present "unsafe" package.
3537
Run(t, files, func(t *testing.T, env *Env) {
3638
env.OpenFile("a.go")
37-
syms := env.Symbol("C")
38-
if got, want := len(syms), 1; got != want {
39-
t.Errorf("got %d symbols, want %d", got, want)
40-
}
39+
checkSymbols(env, "K", "K1")
4140

4241
// Opening up an ignored file will result in an overlay with missing
4342
// metadata, but this shouldn't break workspace symbols requests.
4443
env.OpenFile("exclude.go")
45-
syms = env.Symbol("C")
46-
if got, want := len(syms), 1; got != want {
47-
t.Errorf("got %d symbols, want %d", got, want)
48-
}
44+
checkSymbols(env, "K", "K1")
4945
})
5046
}
5147

@@ -71,15 +67,14 @@ const (
7167
WithOptions(
7268
Settings{"symbolMatcher": symbolMatcher},
7369
).Run(t, files, func(t *testing.T, env *Env) {
74-
want := []string{
70+
checkSymbols(env, "Foo",
7571
"Foo", // prefer exact segment matches first
7672
"FooBar", // ...followed by exact word matches
7773
"Fooex", // shorter than Fooest, FooBar, lexically before Fooey
7874
"Fooey", // shorter than Fooest, Foobar
7975
"Fooest",
80-
}
81-
got := env.Symbol("Foo")
82-
compareSymbols(t, got, want...)
76+
"unsafe.Offsetof", // a very fuzzy match
77+
)
8378
})
8479
}
8580

@@ -102,23 +97,21 @@ const (
10297
WithOptions(
10398
Settings{"symbolMatcher": symbolMatcher},
10499
).Run(t, files, func(t *testing.T, env *Env) {
105-
compareSymbols(t, env.Symbol("ABC"), "ABC", "AxxBxxCxx")
106-
compareSymbols(t, env.Symbol("'ABC"), "ABC")
107-
compareSymbols(t, env.Symbol("^mod.com"), "mod.com/a.ABC", "mod.com/a.AxxBxxCxx")
108-
compareSymbols(t, env.Symbol("^mod.com Axx"), "mod.com/a.AxxBxxCxx")
109-
compareSymbols(t, env.Symbol("C$"), "ABC")
100+
checkSymbols(env, "ABC", "ABC", "AxxBxxCxx")
101+
checkSymbols(env, "'ABC", "ABC")
102+
checkSymbols(env, "^mod.com", "mod.com/a.ABC", "mod.com/a.AxxBxxCxx")
103+
checkSymbols(env, "^mod.com Axx", "mod.com/a.AxxBxxCxx")
104+
checkSymbols(env, "C$", "ABC")
110105
})
111106
}
112107

113-
func compareSymbols(t *testing.T, got []protocol.SymbolInformation, want ...string) {
114-
t.Helper()
115-
if len(got) != len(want) {
116-
t.Errorf("got %d symbols, want %d", len(got), len(want))
108+
func checkSymbols(env *Env, query string, want ...string) {
109+
env.T.Helper()
110+
var got []string
111+
for _, info := range env.Symbol(query) {
112+
got = append(got, info.Name)
117113
}
118-
119-
for i := range got {
120-
if got[i].Name != want[i] {
121-
t.Errorf("got[%d] = %q, want %q", i, got[i].Name, want[i])
122-
}
114+
if diff := cmp.Diff(got, want); diff != "" {
115+
env.T.Errorf("unexpected Symbol(%q) result (+want -got):\n%s", query, diff)
123116
}
124117
}

0 commit comments

Comments
 (0)