blob: 69c9a7417598ca8cf375cbc9066dbcf9dc6e4c9d [file] [log] [blame]
// Copyright 2019 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/token"
"go/types"
"sort"
"sync"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/packages"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/span"
errors "golang.org/x/xerrors"
)
// pkg contains the type information needed by the source package.
type pkg struct {
view *view
// ID and package path have their own types to avoid being used interchangeably.
id packageID
pkgPath packagePath
files []source.ParseGoHandle
errors []packages.Error
imports map[packagePath]*pkg
types *types.Package
typesInfo *types.Info
typesSizes types.Sizes
// The analysis cache holds analysis information for all the packages in a view.
// Each graph node (action) is one unit of analysis.
// Edges express package-to-package (vertical) dependencies,
// and analysis-to-analysis (horizontal) dependencies.
mu sync.Mutex
analyses map[*analysis.Analyzer]*analysisEntry
diagMu sync.Mutex
diagnostics map[*analysis.Analyzer][]source.Diagnostic
}
// packageID is a type that abstracts a package ID.
type packageID string
// packagePath is a type that abstracts a package path.
type packagePath string
type analysisEntry struct {
done chan struct{}
succeeded bool
*source.Action
}
func (pkg *pkg) GetActionGraph(ctx context.Context, a *analysis.Analyzer) (*source.Action, error) {
pkg.mu.Lock()
e, ok := pkg.analyses[a]
if ok {
// cache hit
pkg.mu.Unlock()
// wait for entry to become ready or the context to be cancelled
select {
case <-e.done:
// If the goroutine we are waiting on was cancelled, we should retry.
// If errors other than cancelation/timeout become possible, it may
// no longer be appropriate to always retry here.
if !e.succeeded {
return pkg.GetActionGraph(ctx, a)
}
case <-ctx.Done():
return nil, ctx.Err()
}
} else {
// cache miss
e = &analysisEntry{
done: make(chan struct{}),
Action: &source.Action{
Analyzer: a,
Pkg: pkg,
},
}
pkg.analyses[a] = e
pkg.mu.Unlock()
defer func() {
// If we got an error, clear out our defunct cache entry. We don't cache
// errors since they could depend on our dependencies, which can change.
// Currently the only possible error is context.Canceled, though, which
// should also not be cached.
if !e.succeeded {
pkg.mu.Lock()
delete(pkg.analyses, a)
pkg.mu.Unlock()
}
// Always close done so waiters don't get stuck.
close(e.done)
}()
// This goroutine becomes responsible for populating
// the entry and broadcasting its readiness.
// Add a dependency on each required analyzers.
for _, req := range a.Requires {
act, err := pkg.GetActionGraph(ctx, req)
if err != nil {
return nil, err
}
e.Deps = append(e.Deps, act)
}
// An analysis that consumes/produces facts
// must run on the package's dependencies too.
if len(a.FactTypes) > 0 {
importPaths := make([]string, 0, len(pkg.imports))
for importPath := range pkg.imports {
importPaths = append(importPaths, string(importPath))
}
sort.Strings(importPaths) // for determinism
for _, importPath := range importPaths {
dep, err := pkg.GetImport(ctx, importPath)
if err != nil {
return nil, err
}
act, err := dep.GetActionGraph(ctx, a)
if err != nil {
return nil, err
}
e.Deps = append(e.Deps, act)
}
}
e.succeeded = true
}
return e.Action, nil
}
func (pkg *pkg) ID() string {
return string(pkg.id)
}
func (pkg *pkg) PkgPath() string {
return string(pkg.pkgPath)
}
func (pkg *pkg) GetHandles() []source.ParseGoHandle {
return pkg.files
}
func (pkg *pkg) GetSyntax(ctx context.Context) []*ast.File {
var syntax []*ast.File
for _, ph := range pkg.files {
file, _ := ph.Cached(ctx)
if file != nil {
syntax = append(syntax, file)
}
}
return syntax
}
func (pkg *pkg) GetErrors() []packages.Error {
return pkg.errors
}
func (pkg *pkg) GetTypes() *types.Package {
return pkg.types
}
func (pkg *pkg) GetTypesInfo() *types.Info {
return pkg.typesInfo
}
func (pkg *pkg) GetTypesSizes() types.Sizes {
return pkg.typesSizes
}
func (pkg *pkg) IsIllTyped() bool {
return pkg.types == nil || pkg.typesInfo == nil || pkg.typesSizes == nil
}
func (pkg *pkg) SetDiagnostics(a *analysis.Analyzer, diags []source.Diagnostic) {
pkg.diagMu.Lock()
defer pkg.diagMu.Unlock()
if pkg.diagnostics == nil {
pkg.diagnostics = make(map[*analysis.Analyzer][]source.Diagnostic)
}
pkg.diagnostics[a] = diags
}
func (pkg *pkg) GetDiagnostics() []source.Diagnostic {
pkg.diagMu.Lock()
defer pkg.diagMu.Unlock()
var diags []source.Diagnostic
for _, d := range pkg.diagnostics {
diags = append(diags, d...)
}
return diags
}
func (p *pkg) FindFile(ctx context.Context, uri span.URI, pos token.Pos) (source.ParseGoHandle, *ast.File, source.Package, error) {
queue := []*pkg{p}
seen := make(map[string]bool)
for len(queue) > 0 {
pkg := queue[0]
queue = queue[1:]
seen[pkg.ID()] = true
for _, ph := range pkg.files {
if ph.File().Identity().URI == uri {
file, err := ph.Cached(ctx)
if file == nil {
return nil, nil, nil, err
}
if file.Pos() <= pos && pos <= file.End() {
return ph, file, pkg, nil
}
}
}
for _, dep := range pkg.imports {
if !seen[dep.ID()] {
queue = append(queue, dep)
}
}
}
return nil, nil, nil, errors.Errorf("no file for %s", uri)
}