1
0
mirror of https://github.com/golang/go synced 2024-10-01 07:28:35 -06:00
go/internal/lsp/cache/check.go
Rebecca Stambler b53505e708 internal/lsp: cache analysis using memoize package
This change moves to the approach of caching the analysis using the
memoize package. This means that we will do less work, as we no longer
need to recompute results that are unchanged. The cache key for an
analysis is simply the key of the CheckPackageHandle, along with the
name of the analyzer.

Change-Id: I0e589ccf088ff1de5670401b7207ffa77a254ceb
Reviewed-on: https://go-review.googlesource.com/c/tools/+/200817
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
Reviewed-by: Ian Cottrell <iancottrell@google.com>
2019-10-14 17:19:42 +00:00

387 lines
10 KiB
Go

// 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 (
"bytes"
"context"
"fmt"
"go/ast"
"go/scanner"
"go/types"
"sort"
"sync"
"golang.org/x/tools/go/packages"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/lsp/telemetry"
"golang.org/x/tools/internal/memoize"
"golang.org/x/tools/internal/telemetry/trace"
errors "golang.org/x/xerrors"
)
type importer struct {
snapshot *snapshot
ctx context.Context
// seen maintains the set of previously imported packages.
// If we have seen a package that is already in this map, we have a circular import.
seen map[packageID]struct{}
// topLevelPackageID is the ID of the package from which type-checking began.
topLevelPackageID packageID
// parentPkg is the package that imports the current package.
parentPkg *pkg
// parentCheckPackageHandle is the check package handle that imports the current package.
parentCheckPackageHandle *checkPackageHandle
}
// checkPackageHandle implements source.CheckPackageHandle.
type checkPackageHandle struct {
handle *memoize.Handle
// files are the ParseGoHandles that compose the package.
files []source.ParseGoHandle
// mode is the mode the the files were parsed in.
mode source.ParseMode
// imports is the map of the package's imports.
imports map[packagePath]packageID
// m is the metadata associated with the package.
m *metadata
// key is the hashed key for the package.
key []byte
}
func (cph *checkPackageHandle) packageKey() packageKey {
return packageKey{
id: cph.m.id,
mode: cph.mode,
}
}
// checkPackageData contains the data produced by type-checking a package.
type checkPackageData struct {
memoize.NoCopy
pkg *pkg
err error
}
// checkPackageHandle returns a source.CheckPackageHandle for a given package and config.
func (imp *importer) checkPackageHandle(ctx context.Context, id packageID) (*checkPackageHandle, error) {
// Determine the mode that the files should be parsed in.
mode := imp.mode(id)
// Check if we already have this CheckPackageHandle cached.
if cph := imp.snapshot.getPackage(id, mode); cph != nil {
return cph, nil
}
// Build the CheckPackageHandle for this ID and its dependencies.
cph, err := imp.buildKey(ctx, id, mode)
if err != nil {
return nil, err
}
h := imp.snapshot.view.session.cache.store.Bind(string(cph.key), func(ctx context.Context) interface{} {
data := &checkPackageData{}
data.pkg, data.err = imp.typeCheck(ctx, cph)
return data
})
cph.handle = h
return cph, nil
}
// buildKey computes the checkPackageKey for a given checkPackageHandle.
func (imp *importer) buildKey(ctx context.Context, id packageID, mode source.ParseMode) (*checkPackageHandle, error) {
m := imp.snapshot.getMetadata(id)
if m == nil {
return nil, errors.Errorf("no metadata for %s", id)
}
phs, err := imp.parseGoHandles(ctx, m, mode)
if err != nil {
return nil, err
}
cph := &checkPackageHandle{
m: m,
files: phs,
imports: make(map[packagePath]packageID),
mode: mode,
}
// Make sure all of the deps are sorted.
deps := append([]packageID{}, m.deps...)
sort.Slice(deps, func(i, j int) bool {
return deps[i] < deps[j]
})
// Create the dep importer for use on the dependency handles.
depImporter := &importer{
snapshot: imp.snapshot,
topLevelPackageID: imp.topLevelPackageID,
}
// Begin computing the key by getting the depKeys for all dependencies.
var depKeys [][]byte
for _, dep := range deps {
depHandle, err := depImporter.checkPackageHandle(ctx, dep)
if err != nil {
return nil, errors.Errorf("no dep handle for %s: %+v", dep, err)
}
cph.imports[depHandle.m.pkgPath] = depHandle.m.id
depKeys = append(depKeys, depHandle.key)
}
cph.key = checkPackageKey(cph.m.id, cph.files, m.config, depKeys)
// Cache the CheckPackageHandle in the snapshot.
imp.snapshot.addPackage(cph)
return cph, nil
}
func checkPackageKey(id packageID, phs []source.ParseGoHandle, cfg *packages.Config, deps [][]byte) []byte {
return []byte(hashContents([]byte(fmt.Sprintf("%s%s%s%s", id, hashParseKeys(phs), hashConfig(cfg), hashContents(bytes.Join(deps, nil))))))
}
// hashConfig returns the hash for the *packages.Config.
func hashConfig(config *packages.Config) string {
b := bytes.NewBuffer(nil)
// Dir, Mode, Env, BuildFlags are the parts of the config that can change.
b.WriteString(config.Dir)
b.WriteString(string(config.Mode))
for _, e := range config.Env {
b.WriteString(e)
}
for _, f := range config.BuildFlags {
b.WriteString(f)
}
return hashContents(b.Bytes())
}
func (cph *checkPackageHandle) Check(ctx context.Context) (source.Package, error) {
return cph.check(ctx)
}
func (cph *checkPackageHandle) check(ctx context.Context) (*pkg, error) {
ctx, done := trace.StartSpan(ctx, "cache.checkPackageHandle.check", telemetry.Package.Of(cph.m.id))
defer done()
v := cph.handle.Get(ctx)
if v == nil {
return nil, errors.Errorf("no package for %s", cph.m.id)
}
data := v.(*checkPackageData)
return data.pkg, data.err
}
func (cph *checkPackageHandle) Files() []source.ParseGoHandle {
return cph.files
}
func (cph *checkPackageHandle) ID() string {
return string(cph.m.id)
}
func (cph *checkPackageHandle) MissingDependencies() []string {
var md []string
for i := range cph.m.missingDeps {
md = append(md, string(i))
}
return md
}
func (cph *checkPackageHandle) Cached(ctx context.Context) (source.Package, error) {
return cph.cached(ctx)
}
func (cph *checkPackageHandle) cached(ctx context.Context) (*pkg, error) {
v := cph.handle.Cached()
if v == nil {
return nil, errors.Errorf("no cached type information for %s", cph.m.pkgPath)
}
data := v.(*checkPackageData)
return data.pkg, data.err
}
func (imp *importer) parseGoHandles(ctx context.Context, m *metadata, mode source.ParseMode) ([]source.ParseGoHandle, error) {
phs := make([]source.ParseGoHandle, 0, len(m.files))
for _, uri := range m.files {
f, err := imp.snapshot.view.GetFile(ctx, uri)
if err != nil {
return nil, err
}
fh := imp.snapshot.Handle(ctx, f)
phs = append(phs, imp.snapshot.view.session.cache.ParseGoHandle(fh, mode))
}
return phs, nil
}
func (imp *importer) mode(id packageID) source.ParseMode {
if imp.topLevelPackageID == id {
return source.ParseFull
}
return source.ParseExported
}
func (imp *importer) Import(pkgPath string) (*types.Package, error) {
ctx, done := trace.StartSpan(imp.ctx, "cache.importer.Import", telemetry.PackagePath.Of(pkgPath))
defer done()
// We need to set the parent package's imports, so there should always be one.
if imp.parentPkg == nil {
return nil, errors.Errorf("no parent package for import %s", pkgPath)
}
// Get the CheckPackageHandle from the importing package.
id, ok := imp.parentCheckPackageHandle.imports[packagePath(pkgPath)]
if !ok {
return nil, errors.Errorf("no package data for import path %s", pkgPath)
}
cph := imp.snapshot.getPackage(id, source.ParseExported)
if cph == nil {
return nil, errors.Errorf("no package for %s", id)
}
pkg, err := cph.check(ctx)
if err != nil {
return nil, err
}
imp.parentPkg.imports[packagePath(pkgPath)] = pkg
return pkg.GetTypes(), nil
}
func (imp *importer) typeCheck(ctx context.Context, cph *checkPackageHandle) (*pkg, error) {
ctx, done := trace.StartSpan(ctx, "cache.importer.typeCheck", telemetry.Package.Of(cph.m.id))
defer done()
pkg := &pkg{
view: imp.snapshot.view,
id: cph.m.id,
pkgPath: cph.m.pkgPath,
files: cph.Files(),
imports: make(map[packagePath]*pkg),
typesSizes: cph.m.typesSizes,
typesInfo: &types.Info{
Types: make(map[ast.Expr]types.TypeAndValue),
Defs: make(map[*ast.Ident]types.Object),
Uses: make(map[*ast.Ident]types.Object),
Implicits: make(map[ast.Node]types.Object),
Selections: make(map[*ast.SelectorExpr]*types.Selection),
Scopes: make(map[ast.Node]*types.Scope),
},
}
// If the package comes back with errors from `go list`,
// don't bother type-checking it.
for _, err := range cph.m.errors {
pkg.errors = append(cph.m.errors, err)
}
var (
files = make([]*ast.File, len(pkg.files))
parseErrors = make([]error, len(pkg.files))
wg sync.WaitGroup
)
for i, ph := range pkg.files {
wg.Add(1)
go func(i int, ph source.ParseGoHandle) {
defer wg.Done()
files[i], _, parseErrors[i], _ = ph.Parse(ctx)
}(i, ph)
}
wg.Wait()
for _, err := range parseErrors {
if err != nil {
imp.snapshot.view.session.cache.appendPkgError(pkg, err)
}
}
var i int
for _, f := range files {
if f != nil {
files[i] = f
i++
}
}
files = files[:i]
// Use the default type information for the unsafe package.
if cph.m.pkgPath == "unsafe" {
pkg.types = types.Unsafe
} else if len(files) == 0 { // not the unsafe package, no parsed files
return nil, errors.Errorf("no parsed files for package %s", pkg.pkgPath)
} else {
pkg.types = types.NewPackage(string(cph.m.pkgPath), cph.m.name)
}
cfg := &types.Config{
Error: func(err error) {
imp.snapshot.view.session.cache.appendPkgError(pkg, err)
},
Importer: imp.depImporter(ctx, cph, pkg),
}
check := types.NewChecker(cfg, imp.snapshot.view.session.cache.FileSet(), pkg.types, pkg.typesInfo)
// Type checking errors are handled via the config, so ignore them here.
_ = check.Files(files)
return pkg, nil
}
func (imp *importer) depImporter(ctx context.Context, cph *checkPackageHandle, pkg *pkg) *importer {
// Handle circular imports by copying previously seen imports.
seen := make(map[packageID]struct{})
for k, v := range imp.seen {
seen[k] = v
}
seen[pkg.id] = struct{}{}
return &importer{
snapshot: imp.snapshot,
topLevelPackageID: imp.topLevelPackageID,
parentPkg: pkg,
parentCheckPackageHandle: cph,
seen: seen,
ctx: ctx,
}
}
func (c *cache) appendPkgError(pkg *pkg, err error) {
if err == nil {
return
}
var errs []packages.Error
switch err := err.(type) {
case *scanner.Error:
errs = append(errs, packages.Error{
Pos: err.Pos.String(),
Msg: err.Msg,
Kind: packages.ParseError,
})
case scanner.ErrorList:
// The first parser error is likely the root cause of the problem.
if err.Len() > 0 {
errs = append(errs, packages.Error{
Pos: err[0].Pos.String(),
Msg: err[0].Msg,
Kind: packages.ParseError,
})
}
case types.Error:
errs = append(errs, packages.Error{
Pos: c.FileSet().Position(err.Pos).String(),
Msg: err.Msg,
Kind: packages.TypeError,
})
}
pkg.errors = append(pkg.errors, errs...)
}