blob: 928c3514c31185eb53a5beea6f9939112b294200 [file]
// Copyright 2024 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 fakeloader contains a hermetic loader implementation that does not
// depend on any other systems and can be used in tests.
package fakeloader
import (
"bytes"
"context"
"errors"
"fmt"
"go/ast"
"go/parser"
"go/token"
"go/types"
"strings"
"sync"
"golang.org/x/tools/go/gcexportdata"
"google.golang.org/open2opaque/internal/o2o/loader"
)
// ExportForFunc is a caller-supplied function that returns the export data (.x
// file) for the package with the specified import path.
type ExportForFunc func(importPath string) []byte
type fakeLoader struct {
// fake input from the test
pkgs map[string][]string
files map[string]string
generated map[string]string
exportFor ExportForFunc
}
// NewFakeLoader returns a Loader that can be used in tests. It works in the
// same way as loader returned from NewLoader, except it fakes expensive/flaky
// dependencies (e.g. executing commands).
//
// pkgs maps target rule names (e.g. "//base/go:flag") to list of files in that package
// (e.g. "net/proto2/go/open2opaque/testdata/dummycmd.go"). File paths should be in the form
// expected by blaze, e.g. "//net/proto2/go/open2opaque/testdata:dummy.go". Names of test packages
// should have "_test" suffix.
//
// files maps all files (e.g. "//test/pkg:updated.go"), referenced by the pkgs map, to their content.
// generated maps generated files (e.g. "//test/pkg:generated.go"), referenced by the pkgs map, to ther content.
//
// Unlike the loader returned by NewLoader, the fake loader is inexpensive. It's
// intended that multiple fake loaders are created (perhaps one per test).
func NewFakeLoader(pkgs map[string][]string, files, generated map[string]string, exportFor ExportForFunc) loader.Loader {
l := &fakeLoader{
pkgs: pkgs,
files: files,
generated: generated,
exportFor: exportFor,
}
return l
}
func (fl *fakeLoader) Close(context.Context) error { return nil }
func (fl *fakeLoader) loadPackage(ctx context.Context, target *loader.Target) (_ *loader.Package, err error) {
// target.ID is e.g. google.golang.org/open2opaque/fix/testdata/dummy
files, ok := fl.pkgs[target.ID]
if !ok {
return nil, fmt.Errorf("no such fake package: %q", target.ID)
}
pkgPath := target.ID
pkg := &loader.Package{
Fileset: token.NewFileSet(),
}
var asts []*ast.File
for _, f := range files {
fname := f
generated := false
code, ok := fl.files[f]
if !ok {
if code, ok = fl.generated[f]; !ok {
return nil, errors.New("no source code")
}
generated = true
}
ast, err := parser.ParseFile(pkg.Fileset, fname, code, parser.ParseComments|parser.SpuriousErrors)
if err != nil {
return nil, err
}
asts = append(asts, ast)
pkg.Files = append(pkg.Files, &loader.File{
AST: ast,
Path: fname,
Generated: generated,
Code: code,
})
}
pkg.TypeInfo = &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),
}
imp, err := newFakeImporter(fl.files, fl.generated, fl.exportFor)
if err != nil {
return nil, err
}
cfg := types.Config{
Importer: imp,
FakeImportC: true,
DisableUnusedImportCheck: true,
}
if pkg.TypePkg, err = cfg.Check(pkgPath, pkg.Fileset, asts, pkg.TypeInfo); err != nil {
// Ignore loading errors for the siloedpb package, emulating the
// behavior of the GAP loader with DropSiloedFiles enabled.
if !strings.Contains(err.Error(), "siloedpb") {
return nil, err
}
}
return pkg, nil
}
// LoadPackages loads a batch of Go packages by concurrently calling
// loadPackage() for all targets, as the fake loader does not require any
// synchronization.
func (fl *fakeLoader) LoadPackages(ctx context.Context, targets []*loader.Target, res chan loader.LoadResult) {
var wg sync.WaitGroup
wg.Add(len(targets))
for _, t := range targets {
t := t // copy
go func() {
defer wg.Done()
p, err := fl.loadPackage(ctx, t)
res <- loader.LoadResult{
Target: t,
Package: p,
Err: err,
}
}()
}
wg.Wait()
}
func newFakeImporter(files, generated map[string]string, exportFor ExportForFunc) (*fakeImporter, error) {
imp := &fakeImporter{
checked: make(map[string]*types.Package),
files: files,
generated: generated,
exportFor: exportFor,
}
return imp, nil
}
type fakeImporter struct {
checked map[string]*types.Package
files, generated map[string]string
exportFor ExportForFunc
}
func (imp *fakeImporter) readXFile(importPath string, xFile []byte) (*types.Package, error) {
// Load the package from the .x file if we have not yet loaded it.
r, err := gcexportdata.NewReader(bytes.NewReader(xFile))
if err != nil {
return nil, err
}
pkg, err := gcexportdata.Read(r, token.NewFileSet(), imp.checked, importPath)
if err != nil {
return nil, err
}
imp.checked[importPath] = pkg
return pkg, nil
}
const fakeSync = `
package sync
type Once struct {}
func (*Once) Do(func()) {}
`
const fakeReflect = `
package reflect
type Type interface {
PkgPath() string
}
func TypeOf(any) Type { return nil }
`
const fakeContext = `package context
type Context interface{}
func Background() Context
`
func (imp *fakeImporter) parseAndTypeCheck(pkgPath, fileName, contents string) (*types.Package, error) {
fs := token.NewFileSet()
afile, err := parser.ParseFile(fs, fileName, contents, parser.ParseComments|parser.SpuriousErrors)
if err != nil {
return nil, err
}
cfg := types.Config{Importer: imp}
pkg, err := cfg.Check(pkgPath, fs, []*ast.File{afile}, nil)
if err != nil {
return nil, err
}
imp.checked[pkgPath] = pkg
return pkg, nil
}
func (imp *fakeImporter) Import(pkgPath string) (_ *types.Package, err error) {
if pkgPath == "unsafe" {
return types.Unsafe, nil
}
if p, ok := imp.checked[pkgPath]; ok && p.Complete() {
return p, nil
}
if pkgPath == "sync" {
return imp.parseAndTypeCheck(pkgPath, "sync.go", fakeSync)
}
if pkgPath == "reflect" {
return imp.parseAndTypeCheck(pkgPath, "reflect.go", fakeReflect)
}
if pkgPath == "context" {
return imp.parseAndTypeCheck(pkgPath, "context.go", fakeContext)
}
b := imp.exportFor(pkgPath)
if b == nil {
return nil, fmt.Errorf("tried to load package %q for which there is no export data", pkgPath)
}
return imp.readXFile(pkgPath, b)
}