blob: 9e1c2de12e9076e04bf462206bcfd8939950d600 [file] [log] [blame]
// Copyright 2021 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 symbol
import (
"bytes"
"context"
"encoding/json"
"fmt"
"go/build"
"go/token"
"go/types"
"io"
"log"
"os"
"os/exec"
"path/filepath"
"runtime"
"sort"
"strings"
"sync"
"golang.org/x/pkgsite/internal"
)
// GenerateFeatureContexts computes the exported API for the package specified
// by pkgPath. The source code for that package is in pkgDir.
//
// It is largely adapted from
// https://go.googlesource.com/go/+/refs/heads/master/src/cmd/api/goapi.go.
func GenerateFeatureContexts(ctx context.Context, pkgPath, pkgDir string) (map[string]map[string]bool, error) {
var contexts []*build.Context
for _, c := range internal.BuildContexts {
bc := &build.Context{GOOS: c.GOOS, GOARCH: c.GOARCH}
bc.Compiler = build.Default.Compiler
bc.ReleaseTags = build.Default.ReleaseTags
contexts = append(contexts, bc)
}
var wg sync.WaitGroup
walkers := make([]*Walker, len(internal.BuildContexts))
for i, context := range contexts {
i, context := i, context
wg.Add(1)
go func() {
defer wg.Done()
walkers[i] = NewWalker(context, pkgPath, pkgDir, filepath.Join(build.Default.GOROOT, "src"))
}()
}
wg.Wait()
var featureCtx = make(map[string]map[string]bool) // feature -> context name -> true
for _, w := range walkers {
pkg, err := w.Import(pkgPath)
if _, nogo := err.(*build.NoGoError); nogo {
continue
}
if err != nil {
return nil, fmt.Errorf("import(%q): %v", pkgPath, err)
}
w.export(pkg)
ctxName := contextName(w.context)
for _, f := range w.Features() {
if featureCtx[f] == nil {
featureCtx[f] = make(map[string]bool)
}
featureCtx[f][ctxName] = true
}
}
return featureCtx, nil
}
// FeaturesForVersion returns the set of features introduced at a given
// version.
//
// featureCtx contains all features at this version.
// prevFeatureSet contains all features in the previous version.
// newFeatures contains only features introduced at this version.
// allFeatures contains all features in the package at this version.
func FeaturesForVersion(featureCtx map[string]map[string]bool,
prevFeatureSet map[string]bool) (newFeatures []string, featureSet map[string]bool) {
featureSet = map[string]bool{}
for f, cmap := range featureCtx {
if len(cmap) == len(internal.BuildContexts) {
if !prevFeatureSet[f] {
newFeatures = append(newFeatures, f)
}
featureSet[f] = true
continue
}
comma := strings.Index(f, ",")
for cname := range cmap {
f2 := fmt.Sprintf("%s (%s)%s", f[:comma], cname, f[comma:])
if !prevFeatureSet[f] {
newFeatures = append(newFeatures, f2)
}
featureSet[f2] = true
}
}
return newFeatures, featureSet
}
// export emits the exported package features.
//
// export is the same as
// https://go.googlesource.com/go/+/refs/tags/go1.16.6/src/cmd/api/goapi.go#223
// except verbose mode is removed.
func (w *Walker) export(pkg *types.Package) {
pop := w.pushScope("pkg " + pkg.Path())
w.current = pkg
scope := pkg.Scope()
for _, name := range scope.Names() {
if token.IsExported(name) {
w.emitObj(scope.Lookup(name))
}
}
pop()
}
// Walker is the same as Walkter from
// https://go.googlesource.com/go/+/refs/heads/master/src/cmd/api/goapi.go,
// except Walker.stdPackages was renamed to Walker.packages.
type Walker struct {
context *build.Context
root string
scope []string
current *types.Package
features map[string]bool // set
imported map[string]*types.Package // packages already imported
packages []string // names, omitting "unsafe", internal, and vendored packages
importMap map[string]map[string]string // importer dir -> import path -> canonical path
importDir map[string]string // canonical import path -> dir
}
// NewWalker is the same as
// https://go.googlesource.com/go/+/refs/tags/go1.16.6/src/cmd/api/goapi.go#376,
// except w.context.Dir is set to pkgDir.
func NewWalker(context *build.Context, pkgPath, pkgDir, root string) *Walker {
w := &Walker{
context: context,
root: root,
features: map[string]bool{},
imported: map[string]*types.Package{"unsafe": types.Unsafe},
}
w.context.Dir = pkgDir
w.loadImports(pkgPath)
return w
}
// listImports is the same as
// https://go.googlesource.com/go/+/refs/tags/go1.16.6/src/cmd/api/goapi.go#455,
// but stdPackages was renamed to packages.
type listImports struct {
packages []string // names, omitting "unsafe", internal, and vendored packages
importDir map[string]string // canonical import path → directory
importMap map[string]map[string]string // import path → canonical import path
}
// loadImports is the same as
// https://go.googlesource.com/go/+/refs/tags/go1.16.6/src/cmd/api/goapi.go#483,
// except we accept pkgPath as an argument to check that pkg.ImportPath ==
// pkgPath and retry on various go list errors.
//
// loadImports populates w with information about the packages in the standard
// library and the packages they themselves import in w's build context.
//
// The source import path and expanded import path are identical except for vendored packages.
// For example, on return:
//
// w.importMap["math"] = "math"
// w.importDir["math"] = "<goroot>/src/math"
//
// w.importMap["golang.org/x/net/route"] = "vendor/golang.org/x/net/route"
// w.importDir["vendor/golang.org/x/net/route"] = "<goroot>/src/vendor/golang.org/x/net/route"
//
// Since the set of packages that exist depends on context, the result of
// loadImports also depends on context. However, to improve test running time
// the configuration for each environment is cached across runs.
func (w *Walker) loadImports(pkgPath string) {
if w.context == nil {
return // test-only Walker; does not use the import map
}
generateOutput := func() ([]byte, error) {
cmd := exec.Command(goCmd(), "list", "-e", "-deps", "-json")
cmd.Env = listEnv(w.context)
if w.context.Dir != "" {
cmd.Dir = w.context.Dir
}
return cmd.CombinedOutput()
}
goModDownload := func(out []byte) ([]byte, error) {
words := strings.Fields(string(out))
modPath := words[len(words)-1]
cmd := exec.Command("go", "mod", "download", modPath)
if w.context.Dir != "" {
cmd.Dir = w.context.Dir
}
return cmd.CombinedOutput()
}
retryOrFail := func(out []byte, err error) {
if strings.Contains(string(out), "missing go.sum entry") {
out2, err2 := goModDownload(out)
if err2 != nil {
log.Fatalf("loadImports: initial error: %v\n%s \n\n error running go mod download: %v\n%s",
err, string(out), err2, string(out2))
}
return
}
log.Fatalf("loadImports: %v\n%s", err, out)
}
name := contextName(w.context)
imports, ok := listCache.Load(name)
if !ok {
listSem <- semToken{}
defer func() { <-listSem }()
out, err := generateOutput()
if err != nil {
retryOrFail(out, err)
}
if strings.HasPrefix(string(out), "go: downloading") {
// If a module was downloaded, we will see "go: downloading
// <module> ..." in the JSON output.
// This causes an error in json.NewDecoder below, so run
// generateOutput again to avoid that error.
out, err = generateOutput()
if err != nil {
retryOrFail(out, err)
}
}
var packages []string
importMap := make(map[string]map[string]string)
importDir := make(map[string]string)
dec := json.NewDecoder(bytes.NewReader(out))
for {
var pkg struct {
ImportPath, Dir string
ImportMap map[string]string
Standard bool
}
err := dec.Decode(&pkg)
if err == io.EOF {
break
}
if err != nil {
log.Fatalf("loadImports: go list: invalid output: %v", err)
}
// - Package "unsafe" contains special signatures requiring
// extra care when printing them - ignore since it is not
// going to change w/o a language change.
// - Internal and vendored packages do not contribute to our
// API surface. (If we are running within the "std" module,
// vendored dependencies appear as themselves instead of
// their "vendor/" standard-library copies.)
// - 'go list std' does not include commands, which cannot be
// imported anyway.
if ip := pkg.ImportPath; pkg.ImportPath == pkgPath ||
(pkg.Standard && ip != "unsafe" && !strings.HasPrefix(ip, "vendor/") && !internalPkg.MatchString(ip)) {
packages = append(packages, ip)
}
importDir[pkg.ImportPath] = pkg.Dir
if len(pkg.ImportMap) > 0 {
importMap[pkg.Dir] = make(map[string]string, len(pkg.ImportMap))
}
for k, v := range pkg.ImportMap {
importMap[pkg.Dir][k] = v
}
}
sort.Strings(packages)
imports = listImports{
packages: packages,
importMap: importMap,
importDir: importDir,
}
imports, _ = listCache.LoadOrStore(name, imports)
}
li := imports.(listImports)
w.packages = li.packages
w.importDir = li.importDir
w.importMap = li.importMap
}
// emitStructType is the same as
// https://go.googlesource.com/go/+/refs/tags/go1.16.6/src/cmd/api/goapi.go#931,
// except we also check if a field is Embedded. If so, we ignore that field.
func (w *Walker) emitStructType(name string, typ *types.Struct) {
typeStruct := fmt.Sprintf("type %s struct", name)
w.emitf(typeStruct)
defer w.pushScope(typeStruct)()
for i := 0; i < typ.NumFields(); i++ {
f := typ.Field(i)
if f.Embedded() {
continue
}
if !f.Exported() {
continue
}
typ := f.Type()
if f.Anonymous() {
w.emitf("embedded %s", w.typeString(typ))
continue
}
w.emitf("%s %s", f.Name(), w.typeString(typ))
}
}
// emitIfaceType is the same as
// https://go.googlesource.com/go/+/refs/tags/go1.16.6/src/cmd/api/goapi.go#931,
// except we don't check for unexported methods.
func (w *Walker) emitIfaceType(name string, typ *types.Interface) {
typeInterface := fmt.Sprintf("type " + name + " interface")
w.emitf(typeInterface)
pop := w.pushScope(typeInterface)
var methodNames []string
for i := 0; i < typ.NumExplicitMethods(); i++ {
m := typ.ExplicitMethod(i)
if m.Exported() {
methodNames = append(methodNames, m.Name())
w.emitf("%s%s", m.Name(), w.signatureString(m.Type().(*types.Signature)))
}
}
pop()
sort.Strings(methodNames)
}
// emitf is the same as
// https://go.googlesource.com/go/+/refs/tags/go1.16.6/src/cmd/api/goapi.go#997,
// except verbose mode is removed.
func (w *Walker) emitf(format string, args ...any) {
f := strings.Join(w.scope, ", ") + ", " + fmt.Sprintf(format, args...)
if strings.Contains(f, "\n") {
panic("feature contains newlines: " + f)
}
if _, dup := w.features[f]; dup {
panic("duplicate feature inserted: " + f)
}
w.features[f] = true
}
// goCmd is the same as
// https://go.googlesource.com/go/+/refs/tags/go1.16.6/src/cmd/api/goapi.go#31,
// except support for Windows is removed.
func goCmd() string {
path := filepath.Join(runtime.GOROOT(), "bin", "go")
if _, err := os.Stat(path); err == nil {
return path
}
return "go"
}