aboutsummaryrefslogtreecommitdiff
path: root/internal/lsp/cache/view.go
blob: 5592db4629ee1fa6cac782db9bc2c619cc6107e1 (plain)
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
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package cache

import (
	"context"
	"go/ast"
	"go/parser"
	"go/token"
	"go/types"
	"os"
	"path/filepath"
	"sync"

	"golang.org/x/tools/go/packages"
	"golang.org/x/tools/internal/lsp/debug"
	"golang.org/x/tools/internal/lsp/source"
	"golang.org/x/tools/internal/span"
)

type view struct {
	session *session
	id      string

	// mu protects all mutable state of the view.
	mu sync.Mutex

	// baseCtx is the context handed to NewView. This is the parent of all
	// background contexts created for this view.
	baseCtx context.Context

	// backgroundCtx is the current context used by background tasks initiated
	// by the view.
	backgroundCtx context.Context

	// cancel is called when all action being performed by the current view
	// should be stopped.
	cancel context.CancelFunc

	// Name is the user visible name of this view.
	name string

	// Folder is the root of this view.
	folder span.URI

	// env is the environment to use when invoking underlying tools.
	env []string

	// buildFlags is the build flags to use when invoking underlying tools.
	buildFlags []string

	// keep track of files by uri and by basename, a single file may be mapped
	// to multiple uris, and the same basename may map to multiple files
	filesByURI  map[span.URI]viewFile
	filesByBase map[string][]viewFile

	// mcache caches metadata for the packages of the opened files in a view.
	mcache *metadataCache

	// pcache caches type information for the packages of the opened files in a view.
	pcache *packageCache

	// builtinPkg is the AST package used to resolve builtin types.
	builtinPkg *ast.Package

	// ignoredURIs is the set of URIs of files that we ignore.
	ignoredURIs map[span.URI]struct{}
}

type metadataCache struct {
	mu       sync.Mutex // guards both maps
	packages map[packageID]*metadata
	ids      map[packagePath]packageID
}

type metadata struct {
	id                packageID
	pkgPath           packagePath
	name              string
	files             []string
	typesSizes        types.Sizes
	parents, children map[packageID]bool

	// missingImports is the set of unresolved imports for this package.
	// It contains any packages with `go list` errors.
	missingImports map[packagePath]struct{}
}

type packageCache struct {
	mu       sync.Mutex
	packages map[packageID]*entry
}

type entry struct {
	pkg   *pkg
	err   error
	ready chan struct{} // closed to broadcast ready condition
}

func (v *view) Session() source.Session {
	return v.session
}

// Name returns the user visible name of this view.
func (v *view) Name() string {
	return v.name
}

// Folder returns the root of this view.
func (v *view) Folder() span.URI {
	return v.folder
}

// Config returns the configuration used for the view's interaction with the
// go/packages API. It is shared across all views.
func (v *view) buildConfig() *packages.Config {
	//TODO:should we cache the config and/or overlay somewhere?
	return &packages.Config{
		Context:    v.backgroundCtx,
		Dir:        v.folder.Filename(),
		Env:        v.env,
		BuildFlags: v.buildFlags,
		Mode: packages.NeedName |
			packages.NeedFiles |
			packages.NeedCompiledGoFiles |
			packages.NeedImports |
			packages.NeedDeps |
			packages.NeedTypesSizes,
		Fset:    v.session.cache.fset,
		Overlay: v.session.buildOverlay(),
		ParseFile: func(*token.FileSet, string, []byte) (*ast.File, error) {
			panic("go/packages must not be used to parse files")
		},
		Tests: true,
	}
}

func (v *view) Env() []string {
	v.mu.Lock()
	defer v.mu.Unlock()
	return v.env
}

func (v *view) SetEnv(env []string) {
	v.mu.Lock()
	defer v.mu.Unlock()
	//TODO: this should invalidate the entire view
	v.env = env
}

func (v *view) SetBuildFlags(buildFlags []string) {
	v.mu.Lock()
	defer v.mu.Unlock()
	v.buildFlags = buildFlags
}

func (v *view) Shutdown(ctx context.Context) {
	v.session.removeView(ctx, v)
}

func (v *view) shutdown(context.Context) {
	v.mu.Lock()
	defer v.mu.Unlock()
	if v.cancel != nil {
		v.cancel()
		v.cancel = nil
	}
	debug.DropView(debugView{v})
}

// Ignore checks if the given URI is a URI we ignore.
// As of right now, we only ignore files in the "builtin" package.
func (v *view) Ignore(uri span.URI) bool {
	_, ok := v.ignoredURIs[uri]
	return ok
}

func (v *view) BackgroundContext() context.Context {
	v.mu.Lock()
	defer v.mu.Unlock()

	return v.backgroundCtx
}

func (v *view) BuiltinPackage() *ast.Package {
	return v.builtinPkg
}

// buildBuiltinPkg builds the view's builtin package.
// It assumes that the view is not active yet,
// i.e. it has not been added to the session's list of views.
func (v *view) buildBuiltinPkg() {
	cfg := *v.buildConfig()
	pkgs, _ := packages.Load(&cfg, "builtin")
	if len(pkgs) != 1 {
		v.builtinPkg, _ = ast.NewPackage(cfg.Fset, nil, nil, nil)
		return
	}
	pkg := pkgs[0]
	files := make(map[string]*ast.File)
	for _, filename := range pkg.GoFiles {
		file, err := parser.ParseFile(cfg.Fset, filename, nil, parser.ParseComments)
		if err != nil {
			v.builtinPkg, _ = ast.NewPackage(cfg.Fset, nil, nil, nil)
			return
		}
		files[filename] = file
		v.ignoredURIs[span.NewURI(filename)] = struct{}{}
	}
	v.builtinPkg, _ = ast.NewPackage(cfg.Fset, files, nil, nil)
}

// SetContent sets the overlay contents for a file.
func (v *view) SetContent(ctx context.Context, uri span.URI, content []byte) error {
	v.mu.Lock()
	defer v.mu.Unlock()

	// Cancel all still-running previous requests, since they would be
	// operating on stale data.
	v.cancel()
	v.backgroundCtx, v.cancel = context.WithCancel(v.baseCtx)

	v.session.SetOverlay(uri, content)

	return nil
}

// invalidateContent invalidates the content of a Go file,
// including any position and type information that depends on it.
func (f *goFile) invalidateContent() {
	f.handleMu.Lock()
	defer f.handleMu.Unlock()

	f.invalidateAST()
	f.handle = nil
}

// invalidateAST invalidates the AST of a Go file,
// including any position and type information that depends on it.
func (f *goFile) invalidateAST() {
	f.view.pcache.mu.Lock()
	defer f.view.pcache.mu.Unlock()

	f.ast = nil
	f.token = nil

	// Remove the package and all of its reverse dependencies from the cache.
	if f.pkg != nil {
		f.view.remove(f.pkg.id, map[packageID]struct{}{})
	}
}

// invalidatePackage removes the specified package and dependents from the
// package cache.
func (v *view) invalidatePackage(id packageID) {
	v.pcache.mu.Lock()
	defer v.pcache.mu.Unlock()

	v.remove(id, make(map[packageID]struct{}))
}

// remove invalidates a package and its reverse dependencies in the view's
// package cache. It is assumed that the caller has locked both the mutexes
// of both the mcache and the pcache.
func (v *view) remove(id packageID, seen map[packageID]struct{}) {
	if _, ok := seen[id]; ok {
		return
	}
	m, ok := v.mcache.packages[id]
	if !ok {
		return
	}
	seen[id] = struct{}{}
	for parentID := range m.parents {
		v.remove(parentID, seen)
	}
	// All of the files in the package may also be holding a pointer to the
	// invalidated package.
	for _, filename := range m.files {
		if f, _ := v.findFile(span.FileURI(filename)); f != nil {
			if gof, ok := f.(*goFile); ok {
				gof.pkg = nil
			}
		}
	}
	delete(v.pcache.packages, id)
}

// FindFile returns the file if the given URI is already a part of the view.
func (v *view) FindFile(ctx context.Context, uri span.URI) source.File {
	v.mu.Lock()
	defer v.mu.Unlock()
	f, err := v.findFile(uri)
	if err != nil {
		return nil
	}
	return f
}

// GetFile returns a File for the given URI. It will always succeed because it
// adds the file to the managed set if needed.
func (v *view) GetFile(ctx context.Context, uri span.URI) (source.File, error) {
	v.mu.Lock()
	defer v.mu.Unlock()

	if ctx.Err() != nil {
		return nil, ctx.Err()
	}

	return v.getFile(uri)
}

// getFile is the unlocked internal implementation of GetFile.
func (v *view) getFile(uri span.URI) (viewFile, error) {
	if f, err := v.findFile(uri); err != nil {
		return nil, err
	} else if f != nil {
		return f, nil
	}
	filename := uri.Filename()
	var f viewFile
	switch ext := filepath.Ext(filename); ext {
	case ".mod":
		f = &modFile{
			fileBase: fileBase{
				view:  v,
				fname: filename,
				kind:  source.Mod,
			},
		}
	case ".sum":
		f = &sumFile{
			fileBase: fileBase{
				view:  v,
				fname: filename,
				kind:  source.Sum,
			},
		}
	default:
		// Assume that all other files are Go files, regardless of extension.
		f = &goFile{
			fileBase: fileBase{
				view:  v,
				fname: filename,
				kind:  source.Go,
			},
		}
		v.session.filesWatchMap.Watch(uri, func() {
			f.(*goFile).invalidateContent()
		})
	}
	v.mapFile(uri, f)
	return f, nil
}

// findFile checks the cache for any file matching the given uri.
//
// An error is only returned for an irreparable failure, for example, if the
// filename in question does not exist.
func (v *view) findFile(uri span.URI) (viewFile, error) {
	if f := v.filesByURI[uri]; f != nil {
		// a perfect match
		return f, nil
	}
	// no exact match stored, time to do some real work
	// check for any files with the same basename
	fname := uri.Filename()
	basename := basename(fname)
	if candidates := v.filesByBase[basename]; candidates != nil {
		pathStat, err := os.Stat(fname)
		if os.IsNotExist(err) {
			return nil, err
		} else if err != nil {
			return nil, nil // the file may exist, return without an error
		}
		for _, c := range candidates {
			if cStat, err := os.Stat(c.filename()); err == nil {
				if os.SameFile(pathStat, cStat) {
					// same file, map it
					v.mapFile(uri, c)
					return c, nil
				}
			}
		}
	}
	// no file with a matching name was found, it wasn't in our cache
	return nil, nil
}

func (f *fileBase) addURI(uri span.URI) int {
	f.uris = append(f.uris, uri)
	return len(f.uris)
}

func (v *view) mapFile(uri span.URI, f viewFile) {
	v.filesByURI[uri] = f
	if f.addURI(uri) == 1 {
		basename := basename(f.filename())
		v.filesByBase[basename] = append(v.filesByBase[basename], f)
	}
}

type debugView struct{ *view }

func (v debugView) ID() string             { return v.id }
func (v debugView) Session() debug.Session { return debugSession{v.session} }