| // 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 ...interface{}) { |
| 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" |
| } |