forked from sourcegraph/go-langserver
-
Notifications
You must be signed in to change notification settings - Fork 0
/
workspace_refs.go
261 lines (239 loc) · 7.77 KB
/
workspace_refs.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
package langserver
import (
"context"
"fmt"
"go/build"
"go/parser"
"go/token"
"go/types"
"log"
"runtime"
"sort"
"strconv"
"strings"
"sync"
"golang.org/x/tools/go/buildutil"
"golang.org/x/tools/go/loader"
"github.com/neelance/parallel"
opentracing "github.com/opentracing/opentracing-go"
"github.com/opentracing/opentracing-go/ext"
"github.com/sourcegraph/go-langserver/langserver/internal/refs"
"github.com/sourcegraph/go-langserver/pkg/lspext"
"github.com/sourcegraph/jsonrpc2"
)
func (h *LangHandler) handleWorkspaceReference(ctx context.Context, conn JSONRPC2Conn, req *jsonrpc2.Request, params lspext.WorkspaceReferenceParams) ([]lspext.ReferenceInformation, error) {
rootPath := h.FilePath(h.init.RootPath)
bctx := h.OverlayBuildContext(ctx, h.defaultBuildContext(), !h.init.NoOSFileSystemAccess)
var pkgPat string
if h.init.RootImportPath == "" {
// Go stdlib (empty root import path)
pkgPat = "..."
} else {
// All other Go packages.
pkgPat = h.init.RootImportPath + "/..."
}
var parallelism int
if envWorkspaceReferenceParallelism != "" {
var err error
parallelism, err = strconv.Atoi(envWorkspaceReferenceParallelism)
if err != nil {
return nil, err
}
} else {
parallelism = runtime.NumCPU() / 4 // 1/4 CPU
}
if parallelism < 1 {
parallelism = 1
}
// Perform typechecking.
var (
fset = token.NewFileSet()
pkgs []string
)
for pkg := range buildutil.ExpandPatterns(bctx, []string{pkgPat}) {
// Ignore any vendor package so we can avoid scanning it for external
// references, per the workspace/reference spec. This saves us a
// considerable amount of work.
bpkg, err := bctx.Import(pkg, rootPath, build.FindOnly)
if err != nil && !isMultiplePackageError(err) {
log.Printf("skipping possible package %s: %s", pkg, err)
continue
}
if IsVendorDir(bpkg.Dir) {
continue
}
pkgs = append(pkgs, pkg)
}
prog, err := h.externalRefsTypecheck(ctx, bctx, conn, fset, pkgs)
if err != nil {
return nil, err
}
// Collect external references.
results := refResultSorter{results: make([]lspext.ReferenceInformation, 0)}
par := parallel.NewRun(parallelism)
for _, pkg := range prog.Imported {
par.Acquire()
go func(pkg *loader.PackageInfo) {
defer par.Release()
// Prevent any uncaught panics from taking the entire server down.
defer func() {
if r := recover(); r != nil {
// Same as net/http
const size = 64 << 10
buf := make([]byte, size)
buf = buf[:runtime.Stack(buf, false)]
log.Printf("ignoring panic serving %v for pkg %v: %v\n%s", req.Method, pkg, r, buf)
return
}
}()
err := h.externalRefsFromPkg(ctx, bctx, conn, fset, pkg, rootPath, &results)
if err != nil {
log.Printf("externalRefsFromPkg: %v: %v", pkg, err)
}
}(pkg)
}
_ = par.Wait()
sort.Sort(&results) // sort to provide consistent results
// TODO: We calculate all the results and then throw them away. If we ever
// decide to begin using limiting, we can improve the performance of this
// dramatically. For now, it lives in the spec just so that other
// implementations are aware it may need to be done and to design with that
// in mind.
if len(results.results) > params.Limit && params.Limit > 0 {
results.results = results.results[:params.Limit]
}
return results.results, nil
}
func (h *LangHandler) externalRefsTypecheck(ctx context.Context, bctx *build.Context, conn JSONRPC2Conn, fset *token.FileSet, pkgs []string) (prog *loader.Program, err error) {
span, ctx := opentracing.StartSpanFromContext(ctx, "externalRefsTypecheck")
defer func() {
if err != nil {
ext.Error.Set(span, true)
span.SetTag("err", err.Error())
}
span.Finish()
}()
// Configure the loader.
var typeErrs []error
conf := loader.Config{
Fset: fset,
TypeChecker: types.Config{
DisableUnusedImportCheck: true,
FakeImportC: true,
Error: func(err error) {
typeErrs = append(typeErrs, err)
},
},
Build: bctx,
AllowErrors: true,
ParserMode: parser.AllErrors | parser.ParseComments, // prevent parser from bailing out
FindPackage: func(bctx *build.Context, importPath, fromDir string, mode build.ImportMode) (*build.Package, error) {
// When importing a package, ignore any
// MultipleGoErrors. This occurs, e.g., when you have a
// main.go with "// +build ignore" that imports the
// non-main package in the same dir.
bpkg, err := bctx.Import(importPath, fromDir, mode)
if err != nil && !isMultiplePackageError(err) {
return bpkg, err
}
return bpkg, nil
},
}
for _, path := range pkgs {
conf.Import(path)
}
// Load and typecheck the packages.
prog, err = conf.Load()
if err != nil && prog == nil {
return nil, err
}
// Publish typechecking error diagnostics.
diags, err := errsToDiagnostics(typeErrs, prog)
if err != nil {
return nil, err
}
if len(diags) > 0 {
go func() {
if err := h.publishDiagnostics(ctx, conn, diags); err != nil {
log.Printf("warning: failed to send diagnostics: %s.", err)
}
}()
}
return prog, nil
}
// externalRefsFromPkg collects all the external references from the specified
// package and returns the results.
func (h *LangHandler) externalRefsFromPkg(ctx context.Context, bctx *build.Context, conn JSONRPC2Conn, fs *token.FileSet, pkg *loader.PackageInfo, rootPath string, results *refResultSorter) (err error) {
span, ctx := opentracing.StartSpanFromContext(ctx, "externalRefsFromPkg")
defer func() {
if err != nil {
ext.Error.Set(span, true)
span.SetTag("err", err.Error())
}
span.Finish()
}()
span.SetTag("pkg", pkg)
pkgInWorkspace := func(path string) bool {
return PathHasPrefix(path, h.init.RootImportPath)
}
// Compute external references.
cfg := &refs.Config{
FileSet: fs,
Pkg: pkg.Pkg,
PkgFiles: pkg.Files,
Info: &pkg.Info,
}
refsErr := cfg.Refs(func(r *refs.Ref) {
var defName, defContainerName string
if fields := strings.Fields(r.Def.Path); len(fields) > 0 {
defName = fields[0]
defContainerName = strings.Join(fields[1:], " ")
}
if defContainerName == "" {
defContainerName = r.Def.PackageName
}
defPkg, err := bctx.Import(r.Def.ImportPath, rootPath, build.FindOnly)
if err != nil {
// Log the error, and flag it as one in the trace -- but do not
// halt execution (hopefully, it is limited to a small subset of
// the data).
ext.Error.Set(span, true)
err := fmt.Errorf("externalRefsFromPkg: failed to import %v: %v", r.Def.ImportPath, err)
log.Println(err)
span.SetTag("error", err.Error())
return
}
// If the symbol the reference is to is defined within this workspace,
// exclude it. We only emit refs to symbols that are external to the
// workspace.
if pkgInWorkspace(defPkg.ImportPath) {
return
}
results.resultsMu.Lock()
results.results = append(results.results, lspext.ReferenceInformation{
Name: defName,
ContainerName: defContainerName,
URI: "file://" + defPkg.Dir,
Location: goRangeToLSPLocation(fs, token.Pos(r.Position.Offset), token.Pos(r.Position.Offset+len(defName)-1)),
})
results.resultsMu.Unlock()
})
if refsErr != nil {
// Trace the error, but do not consider it a true error. In many cases
// it is a problem with the user's code, not our external reference
// finding code.
span.SetTag("err", fmt.Sprintf("externalRefsFromPkg: external refs failed: %v: %v", pkg, refsErr))
}
return nil
}
// refResultSorter is a utility struct for collecting, filtering, and
// sorting external reference results.
type refResultSorter struct {
results []lspext.ReferenceInformation
resultsMu sync.Mutex
}
func (s *refResultSorter) Len() int { return len(s.results) }
func (s *refResultSorter) Swap(i, j int) { s.results[i], s.results[j] = s.results[j], s.results[i] }
func (s *refResultSorter) Less(i, j int) bool {
return s.results[i].Location.URI < s.results[j].Location.URI
}