blob: d75205581af215d2c08fc067bc33d15d5a51d83e [file] [log] [blame]
// Copyright 2023 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 typerefs_test
import (
"bytes"
"context"
"flag"
"fmt"
"go/token"
"go/types"
"os"
"sort"
"strings"
"sync"
"testing"
"time"
"golang.org/x/tools/go/gcexportdata"
"golang.org/x/tools/go/packages"
"golang.org/x/tools/gopls/internal/astutil"
"golang.org/x/tools/gopls/internal/lsp/cache"
"golang.org/x/tools/gopls/internal/lsp/source"
"golang.org/x/tools/gopls/internal/lsp/source/typerefs"
"golang.org/x/tools/gopls/internal/span"
"golang.org/x/tools/internal/packagesinternal"
"golang.org/x/tools/internal/testenv"
)
var (
dir = flag.String("dir", "", "dir to run go/packages from")
query = flag.String("query", "std", "go/packages load query to use for walkdecl tests")
verify = flag.Bool("verify", true, "whether to verify reachable packages using export data (may be slow on large graphs)")
)
type (
packageName = source.PackageName
PackageID = source.PackageID
ImportPath = source.ImportPath
PackagePath = source.PackagePath
Metadata = source.Metadata
MetadataSource = source.MetadataSource
ParsedGoFile = source.ParsedGoFile
)
// TestBuildPackageGraph tests the BuildPackageGraph constructor, which uses
// the reference analysis of the Refs function to build a graph of
// relationships between packages.
//
// It simulates the operation of gopls at startup: packages are loaded via
// go/packages, and their syntax+metadata analyzed to determine which packages
// are reachable from others.
//
// The test then verifies that the 'load' graph (the graph of relationships in
// export data) is a subgraph of the 'reach' graph constructed by
// BuildPackageGraph. While doing so, it constructs some statistics about the
// relative sizes of these graphs, along with the 'transitive imports' graph,
// to report the effectiveness of the reachability analysis.
//
// The following flags affect this test:
// - dir sets the dir from which to run go/packages
// - query sets the go/packages query to load
// - verify toggles the verification w.r.t. the load graph (which may be
// prohibitively expensive with large queries).
func TestBuildPackageGraph(t *testing.T) {
if testing.Short() {
t.Skip("skipping with -short: loading the packages can take a long time with a cold cache")
}
testenv.NeedsGoBuild(t) // for go/packages
t0 := time.Now()
exports, meta, err := load(*query, *verify)
if err != nil {
t.Fatalf("loading failed: %v", err)
}
t.Logf("loaded %d packages in %v", len(exports), time.Since(t0))
ctx := context.Background()
var ids []PackageID
for id := range exports {
ids = append(ids, id)
}
sort.Slice(ids, func(i, j int) bool {
return ids[i] < ids[j]
})
t0 = time.Now()
g, err := BuildPackageGraph(ctx, meta, ids, newParser().parse)
if err != nil {
t.Fatal(err)
}
t.Logf("building package graph took %v", time.Since(t0))
// Collect information about the edges between packages for later analysis.
//
// We compare the following package graphs:
// - the imports graph: edges are transitive imports
// - the reaches graph: edges are reachability relationships through syntax
// of imports (as defined in the package doc)
// - the loads graph: edges are packages loaded through the export data of
// imports
//
// By definition, loads < reaches < imports.
type edgeSet map[PackageID]map[PackageID]bool
var (
imports = make(edgeSet) // A imports B transitively
importedBy = make(edgeSet) // A is imported by B transitively
reaches = make(edgeSet) // A reaches B through top-level declaration syntax
reachedBy = make(edgeSet) // A is reached by B through top-level declaration syntax
loads = make(edgeSet) // A loads B through export data of its direct dependencies
loadedBy = make(edgeSet) // A is loaded by B through export data of B's direct dependencies
)
recordEdge := func(from, to PackageID, fwd, rev edgeSet) {
if fwd[from] == nil {
fwd[from] = make(map[PackageID]bool)
}
fwd[from][to] = true
if rev[to] == nil {
rev[to] = make(map[PackageID]bool)
}
rev[to][from] = true
}
exportedPackages := make(map[PackageID]*types.Package)
importPackage := func(id PackageID) *types.Package {
exportFile := exports[id]
if exportFile == "" {
return nil // no exported symbols
}
m := meta.Metadata(id)
tpkg, ok := exportedPackages[id]
if !ok {
pkgPath := string(m.PkgPath)
tpkg, err = importFromExportData(pkgPath, exportFile)
if err != nil {
t.Fatalf("importFromExportData(%s, %s) failed: %v", pkgPath, exportFile, err)
}
exportedPackages[id] = tpkg
}
return tpkg
}
for _, id := range ids {
pkg, err := g.Package(ctx, id)
if err != nil {
t.Fatal(err)
}
pkg.ReachesByDeps.Elems(func(id2 typerefs.IndexID) {
recordEdge(id, g.pkgIndex.PackageID(id2), reaches, reachedBy)
})
importMap := importMap(id, meta)
for _, id2 := range importMap {
recordEdge(id, id2, imports, importedBy)
}
if *verify {
for _, depID := range meta.Metadata(id).DepsByPkgPath {
tpkg := importPackage(depID)
if tpkg == nil {
continue
}
for _, imp := range tpkg.Imports() {
depID, ok := importMap[PackagePath(imp.Path())]
if !ok {
t.Errorf("import map (len: %d) for %s missing imported types.Package %s", len(importMap), id, imp.Path())
continue
}
recordEdge(id, depID, loads, loadedBy)
}
}
for depID := range loads[id] {
if !pkg.ReachesByDeps.Contains(depID) {
t.Errorf("package %s was imported by %s, but not detected as reachable", depID, id)
}
}
}
}
if testing.Verbose() {
fmt.Printf("%-52s%8s%8s%8s%8s%8s%8s\n", "package ID", "imp", "impBy", "reach", "reachBy", "load", "loadBy")
for _, id := range ids {
fmt.Printf("%-52s%8d%8d%8d%8d%8d%8d\n", id, len(imports[id]), len(importedBy[id]), len(reaches[id]), len(reachedBy[id]), len(loads[id]), len(loadedBy[id]))
}
fmt.Println(strings.Repeat("-", 100))
fmt.Printf("%-52s%8s%8s%8s%8s%8s%8s\n", "package ID", "imp", "impBy", "reach", "reachBy", "load", "loadBy")
avg := func(m edgeSet) float64 {
var avg float64
for _, id := range ids {
s := m[id]
avg += float64(len(s)) / float64(len(ids))
}
return avg
}
fmt.Printf("%52s%8.1f%8.1f%8.1f%8.1f%8.1f%8.1f\n", "averages:", avg(imports), avg(importedBy), avg(reaches), avg(reachedBy), avg(loads), avg(loadedBy))
}
}
func importMap(id PackageID, meta MetadataSource) map[PackagePath]PackageID {
imports := make(map[PackagePath]PackageID)
var recordIDs func(PackageID)
recordIDs = func(id PackageID) {
m := meta.Metadata(id)
if _, ok := imports[m.PkgPath]; ok {
return
}
imports[m.PkgPath] = id
for _, id := range m.DepsByPkgPath {
recordIDs(id)
}
}
for _, id := range meta.Metadata(id).DepsByPkgPath {
recordIDs(id)
}
return imports
}
func importFromExportData(pkgPath, exportFile string) (*types.Package, error) {
file, err := os.Open(exportFile)
if err != nil {
return nil, err
}
r, err := gcexportdata.NewReader(file)
if err != nil {
file.Close()
return nil, err
}
fset := token.NewFileSet()
tpkg, err := gcexportdata.Read(r, fset, make(map[string]*types.Package), pkgPath)
file.Close()
if err != nil {
return nil, err
}
// The export file reported by go/packages is produced by the compiler, which
// has additional package dependencies due to inlining.
//
// Export and re-import so that we only observe dependencies from the
// exported API.
var out bytes.Buffer
err = gcexportdata.Write(&out, fset, tpkg)
if err != nil {
return nil, err
}
return gcexportdata.Read(&out, token.NewFileSet(), make(map[string]*types.Package), pkgPath)
}
func BenchmarkBuildPackageGraph(b *testing.B) {
t0 := time.Now()
exports, meta, err := load(*query, *verify)
if err != nil {
b.Fatalf("loading failed: %v", err)
}
b.Logf("loaded %d packages in %v", len(exports), time.Since(t0))
ctx := context.Background()
var ids []PackageID
for id := range exports {
ids = append(ids, id)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := BuildPackageGraph(ctx, meta, ids, newParser().parse)
if err != nil {
b.Fatal(err)
}
}
}
type memoizedParser struct {
mu sync.Mutex
files map[span.URI]*futureParse
}
type futureParse struct {
done chan struct{}
pgf *ParsedGoFile
err error
}
func newParser() *memoizedParser {
return &memoizedParser{
files: make(map[span.URI]*futureParse),
}
}
func (p *memoizedParser) parse(ctx context.Context, uri span.URI) (*ParsedGoFile, error) {
doParse := func(ctx context.Context, uri span.URI) (*ParsedGoFile, error) {
// TODO(adonovan): hoist this operation outside the benchmark critsec.
content, err := os.ReadFile(uri.Filename())
if err != nil {
return nil, err
}
content = astutil.PurgeFuncBodies(content)
pgf, _ := cache.ParseGoSrc(ctx, token.NewFileSet(), uri, content, source.ParseFull, false)
return pgf, nil
}
p.mu.Lock()
fut, ok := p.files[uri]
if ok {
p.mu.Unlock()
select {
case <-fut.done:
case <-ctx.Done():
return nil, ctx.Err()
}
} else {
fut = &futureParse{done: make(chan struct{})}
p.files[uri] = fut
p.mu.Unlock()
fut.pgf, fut.err = doParse(ctx, uri)
close(fut.done)
}
return fut.pgf, fut.err
}
type mapMetadataSource struct {
m map[PackageID]*Metadata
}
func (s mapMetadataSource) Metadata(id PackageID) *Metadata {
return s.m[id]
}
// This function is a compressed version of snapshot.load from the
// internal/lsp/cache package, for use in testing.
//
// TODO(rfindley): it may be valuable to extract this logic from the snapshot,
// since it is otherwise standalone.
func load(query string, needExport bool) (map[PackageID]string, MetadataSource, error) {
cfg := &packages.Config{
Dir: *dir,
Mode: packages.NeedName |
packages.NeedFiles |
packages.NeedCompiledGoFiles |
packages.NeedImports |
packages.NeedDeps |
packages.NeedTypesSizes |
packages.NeedModule |
packages.NeedEmbedFiles |
packages.LoadMode(packagesinternal.DepsErrors) |
packages.LoadMode(packagesinternal.ForTest),
Tests: true,
}
if needExport {
cfg.Mode |= packages.NeedExportFile // ExportFile is not requested by gopls: this is used to verify reachability
}
pkgs, err := packages.Load(cfg, query)
if err != nil {
return nil, nil, err
}
meta := make(map[PackageID]*Metadata)
var buildMetadata func(pkg *packages.Package)
buildMetadata = func(pkg *packages.Package) {
id := PackageID(pkg.ID)
if meta[id] != nil {
return
}
m := &Metadata{
ID: id,
PkgPath: PackagePath(pkg.PkgPath),
Name: packageName(pkg.Name),
ForTest: PackagePath(packagesinternal.GetForTest(pkg)),
TypesSizes: pkg.TypesSizes,
LoadDir: cfg.Dir,
Module: pkg.Module,
Errors: pkg.Errors,
DepsErrors: packagesinternal.GetDepsErrors(pkg),
}
meta[id] = m
for _, filename := range pkg.CompiledGoFiles {
m.CompiledGoFiles = append(m.CompiledGoFiles, span.URIFromPath(filename))
}
for _, filename := range pkg.GoFiles {
m.GoFiles = append(m.GoFiles, span.URIFromPath(filename))
}
m.DepsByImpPath = make(map[ImportPath]PackageID)
m.DepsByPkgPath = make(map[PackagePath]PackageID)
for importPath, imported := range pkg.Imports {
importPath := ImportPath(importPath)
// see note in gopls/internal/lsp/cache/load.go for an explanation of this check.
if importPath != "unsafe" && len(imported.CompiledGoFiles) == 0 {
m.DepsByImpPath[importPath] = "" // missing
continue
}
m.DepsByImpPath[importPath] = PackageID(imported.ID)
m.DepsByPkgPath[PackagePath(imported.PkgPath)] = PackageID(imported.ID)
buildMetadata(imported)
}
}
exportFiles := make(map[PackageID]string)
for _, pkg := range pkgs {
exportFiles[PackageID(pkg.ID)] = pkg.ExportFile
buildMetadata(pkg)
}
return exportFiles, &mapMetadataSource{meta}, nil
}