blob: 841b368c9d7871cc818178ab57ad2583368fa5e0 [file] [log] [blame]
// Copyright 2022 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 gcimporter_test
import (
"fmt"
"go/ast"
"go/parser"
"go/token"
"go/types"
"os"
"strings"
"testing"
"golang.org/x/sync/errgroup"
"golang.org/x/tools/go/packages"
"golang.org/x/tools/internal/gcimporter"
"golang.org/x/tools/internal/testenv"
)
// TestStd type-checks the standard library using shallow export data.
func TestShallowStd(t *testing.T) {
if testing.Short() {
t.Skip("skipping in short mode; too slow (https://golang.org/issue/14113)")
}
testenv.NeedsTool(t, "go")
// Load import graph of the standard library.
// (No parsing or type-checking.)
cfg := &packages.Config{
Mode: packages.NeedImports |
packages.NeedName |
packages.NeedFiles | // see https://github.com/golang/go/issues/56632
packages.NeedCompiledGoFiles,
Tests: false,
}
pkgs, err := packages.Load(cfg, "std")
if err != nil {
t.Fatalf("load: %v", err)
}
if len(pkgs) < 200 {
t.Fatalf("too few packages: %d", len(pkgs))
}
// Type check the packages in parallel postorder.
done := make(map[*packages.Package]chan struct{})
packages.Visit(pkgs, nil, func(p *packages.Package) {
done[p] = make(chan struct{})
})
packages.Visit(pkgs, nil,
func(pkg *packages.Package) {
go func() {
// Wait for all deps to be done.
for _, imp := range pkg.Imports {
<-done[imp]
}
typecheck(t, pkg)
close(done[pkg])
}()
})
for _, root := range pkgs {
<-done[root]
}
}
// typecheck reads, parses, and type-checks a package.
// It squirrels the export data in the ppkg.ExportFile field.
func typecheck(t *testing.T, ppkg *packages.Package) {
if ppkg.PkgPath == "unsafe" {
return // unsafe is special
}
// Create a local FileSet just for this package.
fset := token.NewFileSet()
// Parse files in parallel.
syntax := make([]*ast.File, len(ppkg.CompiledGoFiles))
var group errgroup.Group
for i, filename := range ppkg.CompiledGoFiles {
i, filename := i, filename
group.Go(func() error {
f, err := parser.ParseFile(fset, filename, nil, parser.SkipObjectResolution)
if err != nil {
return err // e.g. missing file
}
syntax[i] = f
return nil
})
}
if err := group.Wait(); err != nil {
t.Fatal(err)
}
// Inv: all files were successfully parsed.
// Build map of dependencies by package path.
// (We don't compute this mapping for the entire
// packages graph because it is not globally consistent.)
depsByPkgPath := make(map[string]*packages.Package)
{
var visit func(*packages.Package)
visit = func(pkg *packages.Package) {
if depsByPkgPath[pkg.PkgPath] == nil {
depsByPkgPath[pkg.PkgPath] = pkg
for path := range pkg.Imports {
visit(pkg.Imports[path])
}
}
}
visit(ppkg)
}
// importer state
var (
loadFromExportData func(*packages.Package) (*types.Package, error)
importMap = map[string]*types.Package{ // keys are PackagePaths
ppkg.PkgPath: types.NewPackage(ppkg.PkgPath, ppkg.Name),
}
)
loadFromExportData = func(imp *packages.Package) (*types.Package, error) {
export := []byte(imp.ExportFile)
getPackages := func(items []gcimporter.GetPackagesItem) error {
for i, item := range items {
pkg, ok := importMap[item.Path]
if !ok {
dep, ok := depsByPkgPath[item.Path]
if !ok {
return fmt.Errorf("can't find dependency: %q", item.Path)
}
pkg = types.NewPackage(item.Path, dep.Name)
importMap[item.Path] = pkg
loadFromExportData(dep) // side effect: populate package scope
}
items[i].Pkg = pkg
}
return nil
}
return gcimporter.IImportShallow(fset, getPackages, export, imp.PkgPath, nil)
}
// Type-check the syntax trees.
cfg := &types.Config{
Error: func(e error) {
t.Error(e)
},
Importer: importerFunc(func(importPath string) (*types.Package, error) {
if importPath == "unsafe" {
return types.Unsafe, nil // unsafe has no exportdata
}
imp, ok := ppkg.Imports[importPath]
if !ok {
return nil, fmt.Errorf("missing import %q", importPath)
}
return loadFromExportData(imp)
}),
}
// (Use NewChecker+Files to ensure Package.Name is set explicitly.)
tpkg := types.NewPackage(ppkg.PkgPath, ppkg.Name)
_ = types.NewChecker(cfg, fset, tpkg, nil).Files(syntax) // ignore error
// Check sanity.
postTypeCheck(t, fset, tpkg)
// Save the export data.
data, err := gcimporter.IExportShallow(fset, tpkg, nil)
if err != nil {
t.Fatalf("internal error marshalling export data: %v", err)
}
ppkg.ExportFile = string(data)
}
// postTypeCheck is called after a package is type checked.
// We use it to assert additional correctness properties,
// for example, that the apparent location of "fmt.Println"
// corresponds to its source location: in other words,
// export+import preserves high-fidelity positions.
func postTypeCheck(t *testing.T, fset *token.FileSet, pkg *types.Package) {
// We hard-code a few interesting test-case objects.
var obj types.Object
switch pkg.Path() {
case "fmt":
// func fmt.Println
obj = pkg.Scope().Lookup("Println")
case "net/http":
// method (*http.Request).ParseForm
req := pkg.Scope().Lookup("Request")
obj, _, _ = types.LookupFieldOrMethod(req.Type(), true, pkg, "ParseForm")
default:
return
}
if obj == nil {
t.Errorf("object not found in package %s", pkg.Path())
return
}
// Now check the source fidelity of the object's position.
posn := fset.Position(obj.Pos())
data, err := os.ReadFile(posn.Filename)
if err != nil {
t.Errorf("can't read source file declaring %v: %v", obj, err)
return
}
// Check line and column denote a source interval containing the object's identifier.
line := strings.Split(string(data), "\n")[posn.Line-1]
if id := line[posn.Column-1 : posn.Column-1+len(obj.Name())]; id != obj.Name() {
t.Errorf("%+v: expected declaration of %v at this line, column; got %q", posn, obj, line)
}
// Check offset.
if id := string(data[posn.Offset : posn.Offset+len(obj.Name())]); id != obj.Name() {
t.Errorf("%+v: expected declaration of %v at this offset; got %q", posn, obj, id)
}
// Check commutativity of Position() and start+len(name) operations:
// Position(startPos+len(name)) == Position(startPos) + len(name).
// This important property is a consequence of the way in which the
// decoder fills the gaps in the sparse line-start offset table.
endPosn := fset.Position(obj.Pos() + token.Pos(len(obj.Name())))
wantEndPosn := token.Position{
Filename: posn.Filename,
Offset: posn.Offset + len(obj.Name()),
Line: posn.Line,
Column: posn.Column + len(obj.Name()),
}
if endPosn != wantEndPosn {
t.Errorf("%+v: expected end Position of %v here; was at %+v", wantEndPosn, obj, endPosn)
}
}