Skip to content

Commit 2bed671

Browse files
committed
tests/api,internal/symbol: add generate script
A script is add which generates the API for a package from its source code, and writes this data to tests/api/testdata. The script is largely adapted from cmd/api/goapi.go. The resulting testdata will be used in following CLs to compare against the symbol history data on the frontend. For golang/go#37102 Change-Id: Iae3d9b6f55de94bdd81821f374aaf53276f306b9 Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/339452 Trust: Julie Qiu <[email protected]> Run-TryBot: Julie Qiu <[email protected]> TryBot-Result: kokoro <[email protected]> Reviewed-by: Jonathan Amsterdam <[email protected]>
1 parent 0f5a3b7 commit 2bed671

File tree

7 files changed

+1042
-0
lines changed

7 files changed

+1042
-0
lines changed

internal/symbol/generate.go

Lines changed: 334 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,334 @@
1+
// Copyright 2021 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package symbol
6+
7+
import (
8+
"bytes"
9+
"context"
10+
"encoding/json"
11+
"fmt"
12+
"go/build"
13+
"go/token"
14+
"go/types"
15+
"io"
16+
"log"
17+
"os"
18+
"os/exec"
19+
"path/filepath"
20+
"runtime"
21+
"sort"
22+
"strings"
23+
"sync"
24+
25+
"golang.org/x/pkgsite/internal"
26+
)
27+
28+
// GenerateFeatureContexts computes the exported API for the package specified
29+
// by pkgPath. The source code for that package is in pkgDir.
30+
//
31+
// It is largely adapted from
32+
// https://go.googlesource.com/go/+/refs/heads/master/src/cmd/api/goapi.go.
33+
func GenerateFeatureContexts(ctx context.Context, pkgPath, pkgDir string) (map[string]map[string]bool, error) {
34+
var contexts []*build.Context
35+
for _, c := range internal.BuildContexts {
36+
bc := &build.Context{GOOS: c.GOOS, GOARCH: c.GOARCH}
37+
bc.Compiler = build.Default.Compiler
38+
bc.ReleaseTags = build.Default.ReleaseTags
39+
contexts = append(contexts, bc)
40+
}
41+
42+
var wg sync.WaitGroup
43+
walkers := make([]*Walker, len(internal.BuildContexts))
44+
for i, context := range contexts {
45+
i, context := i, context
46+
wg.Add(1)
47+
go func() {
48+
defer wg.Done()
49+
walkers[i] = NewWalker(context, pkgPath, pkgDir, filepath.Join(build.Default.GOROOT, "src"))
50+
}()
51+
}
52+
wg.Wait()
53+
var featureCtx = make(map[string]map[string]bool) // feature -> context name -> true
54+
for _, w := range walkers {
55+
pkg, err := w.Import(pkgPath)
56+
if _, nogo := err.(*build.NoGoError); nogo {
57+
continue
58+
}
59+
if err != nil {
60+
return nil, fmt.Errorf("import(%q): %v", pkgPath, err)
61+
}
62+
w.export(pkg)
63+
ctxName := contextName(w.context)
64+
for _, f := range w.Features() {
65+
if featureCtx[f] == nil {
66+
featureCtx[f] = make(map[string]bool)
67+
}
68+
featureCtx[f][ctxName] = true
69+
}
70+
}
71+
return featureCtx, nil
72+
}
73+
74+
// FeaturesForVersion returns the set of features introduced at a given
75+
// version.
76+
//
77+
// featureCtx contains all features at this version.
78+
// prevFeatureSet contains all features in the previous version.
79+
// newFeatures contains only features introduced at this version.
80+
// allFeatures contains all features in the package at this version.
81+
func FeaturesForVersion(featureCtx map[string]map[string]bool,
82+
prevFeatureSet map[string]bool) (newFeatures []string, featureSet map[string]bool) {
83+
featureSet = map[string]bool{}
84+
for f, cmap := range featureCtx {
85+
if len(cmap) == len(internal.BuildContexts) {
86+
if !prevFeatureSet[f] {
87+
newFeatures = append(newFeatures, f)
88+
}
89+
featureSet[f] = true
90+
continue
91+
}
92+
comma := strings.Index(f, ",")
93+
for cname := range cmap {
94+
f2 := fmt.Sprintf("%s (%s)%s", f[:comma], cname, f[comma:])
95+
if !prevFeatureSet[f] {
96+
newFeatures = append(newFeatures, f2)
97+
}
98+
featureSet[f2] = true
99+
}
100+
}
101+
return newFeatures, featureSet
102+
}
103+
104+
// export emits the exported package features.
105+
//
106+
// export is the same as
107+
// https://go.googlesource.com/go/+/refs/tags/go1.16.6/src/cmd/api/goapi.go#223
108+
// except verbose mode is removed.
109+
func (w *Walker) export(pkg *types.Package) {
110+
pop := w.pushScope("pkg " + pkg.Path())
111+
w.current = pkg
112+
scope := pkg.Scope()
113+
for _, name := range scope.Names() {
114+
if token.IsExported(name) {
115+
w.emitObj(scope.Lookup(name))
116+
}
117+
}
118+
pop()
119+
}
120+
121+
// Walker is the same as Walkter from
122+
// https://go.googlesource.com/go/+/refs/heads/master/src/cmd/api/goapi.go,
123+
// except Walker.stdPackages was renamed to Walker.packages.
124+
type Walker struct {
125+
context *build.Context
126+
root string
127+
scope []string
128+
current *types.Package
129+
features map[string]bool // set
130+
imported map[string]*types.Package // packages already imported
131+
packages []string // names, omitting "unsafe", internal, and vendored packages
132+
importMap map[string]map[string]string // importer dir -> import path -> canonical path
133+
importDir map[string]string // canonical import path -> dir
134+
}
135+
136+
// NewWalker is the same as
137+
// https://go.googlesource.com/go/+/refs/tags/go1.16.6/src/cmd/api/goapi.go#376,
138+
// except w.context.Dir is set to pkgDir.
139+
func NewWalker(context *build.Context, pkgPath, pkgDir, root string) *Walker {
140+
w := &Walker{
141+
context: context,
142+
root: root,
143+
features: map[string]bool{},
144+
imported: map[string]*types.Package{"unsafe": types.Unsafe},
145+
}
146+
w.context.Dir = pkgDir
147+
w.loadImports(pkgPath)
148+
return w
149+
}
150+
151+
// listImports is the same as
152+
// https://go.googlesource.com/go/+/refs/tags/go1.16.6/src/cmd/api/goapi.go#455,
153+
// but stdPackages was renamed to packages.
154+
type listImports struct {
155+
packages []string // names, omitting "unsafe", internal, and vendored packages
156+
importDir map[string]string // canonical import path → directory
157+
importMap map[string]map[string]string // import path → canonical import path
158+
}
159+
160+
// loadImports populates w with information about the packages in the standard
161+
// library and the packages they themselves import in w's build context.
162+
//
163+
// The source import path and expanded import path are identical except for vendored packages.
164+
// For example, on return:
165+
//
166+
// w.importMap["math"] = "math"
167+
// w.importDir["math"] = "<goroot>/src/math"
168+
//
169+
// w.importMap["golang.org/x/net/route"] = "vendor/golang.org/x/net/route"
170+
// w.importDir["vendor/golang.org/x/net/route"] = "<goroot>/src/vendor/golang.org/x/net/route"
171+
//
172+
// Since the set of packages that exist depends on context, the result of
173+
// loadImports also depends on context. However, to improve test running time
174+
// the configuration for each environment is cached across runs.
175+
//
176+
// loadImports is the same as
177+
// https://go.googlesource.com/go/+/refs/tags/go1.16.6/src/cmd/api/goapi.go#483,
178+
// except we accept pkgPath as an argument to check that pkg.ImportPath ==
179+
// pkgPath.
180+
func (w *Walker) loadImports(pkgPath string) {
181+
if w.context == nil {
182+
return // test-only Walker; does not use the import map
183+
}
184+
name := contextName(w.context)
185+
imports, ok := listCache.Load(name)
186+
187+
generateOutput := func() ([]byte, error) {
188+
cmd := exec.Command(goCmd(), "list", "-e", "-deps", "-json")
189+
cmd.Env = listEnv(w.context)
190+
if w.context.Dir != "" {
191+
cmd.Dir = w.context.Dir
192+
}
193+
return cmd.CombinedOutput()
194+
}
195+
if !ok {
196+
listSem <- semToken{}
197+
defer func() { <-listSem }()
198+
out, err := generateOutput()
199+
if err != nil {
200+
if strings.Contains(string(out), "missing go.sum entry") {
201+
words := strings.Fields(string(out))
202+
modPath := words[len(words)-1]
203+
cmd := exec.Command("go", "mod", "download", modPath)
204+
cmd.Dir = w.context.Dir
205+
out2, err2 := cmd.CombinedOutput()
206+
if err2 != nil {
207+
log.Fatalf("loadImports: initial error: %v\n%s \n\n error running go mod download: %v\n%s",
208+
err, string(out), err2, string(out2))
209+
}
210+
} else {
211+
log.Fatalf("loadImports: %v\n%s", err, out)
212+
}
213+
}
214+
var packages []string
215+
importMap := make(map[string]map[string]string)
216+
importDir := make(map[string]string)
217+
dec := json.NewDecoder(bytes.NewReader(out))
218+
for {
219+
var pkg struct {
220+
ImportPath, Dir string
221+
ImportMap map[string]string
222+
Standard bool
223+
}
224+
err := dec.Decode(&pkg)
225+
if err == io.EOF {
226+
break
227+
}
228+
if err != nil {
229+
log.Fatalf("go list: invalid output: %v", err)
230+
}
231+
// - Package "unsafe" contains special signatures requiring
232+
// extra care when printing them - ignore since it is not
233+
// going to change w/o a language change.
234+
// - Internal and vendored packages do not contribute to our
235+
// API surface. (If we are running within the "std" module,
236+
// vendored dependencies appear as themselves instead of
237+
// their "vendor/" standard-library copies.)
238+
// - 'go list std' does not include commands, which cannot be
239+
// imported anyway.
240+
if ip := pkg.ImportPath; pkg.ImportPath == pkgPath ||
241+
(pkg.Standard && ip != "unsafe" && !strings.HasPrefix(ip, "vendor/") && !internalPkg.MatchString(ip)) {
242+
packages = append(packages, ip)
243+
}
244+
importDir[pkg.ImportPath] = pkg.Dir
245+
if len(pkg.ImportMap) > 0 {
246+
importMap[pkg.Dir] = make(map[string]string, len(pkg.ImportMap))
247+
}
248+
for k, v := range pkg.ImportMap {
249+
importMap[pkg.Dir][k] = v
250+
}
251+
}
252+
sort.Strings(packages)
253+
imports = listImports{
254+
packages: packages,
255+
importMap: importMap,
256+
importDir: importDir,
257+
}
258+
imports, _ = listCache.LoadOrStore(name, imports)
259+
}
260+
li := imports.(listImports)
261+
w.packages = li.packages
262+
w.importDir = li.importDir
263+
w.importMap = li.importMap
264+
}
265+
266+
// emitStructType is the same as
267+
// https://go.googlesource.com/go/+/refs/tags/go1.16.6/src/cmd/api/goapi.go#931,
268+
// except we also check if a field is Embedded. If so, we ignore that field.
269+
func (w *Walker) emitStructType(name string, typ *types.Struct) {
270+
typeStruct := fmt.Sprintf("type %s struct", name)
271+
w.emitf(typeStruct)
272+
defer w.pushScope(typeStruct)()
273+
for i := 0; i < typ.NumFields(); i++ {
274+
f := typ.Field(i)
275+
if f.Embedded() {
276+
continue
277+
}
278+
if !f.Exported() {
279+
continue
280+
}
281+
typ := f.Type()
282+
if f.Anonymous() {
283+
w.emitf("embedded %s", w.typeString(typ))
284+
continue
285+
}
286+
w.emitf("%s %s", f.Name(), w.typeString(typ))
287+
}
288+
}
289+
290+
// emitIfaceType is the same as
291+
// https://go.googlesource.com/go/+/refs/tags/go1.16.6/src/cmd/api/goapi.go#931,
292+
// except we don't check for unexported methods.
293+
func (w *Walker) emitIfaceType(name string, typ *types.Interface) {
294+
typeInterface := fmt.Sprintf("type " + name + " interface")
295+
w.emitf(typeInterface)
296+
pop := w.pushScope(typeInterface)
297+
298+
var methodNames []string
299+
for i := 0; i < typ.NumExplicitMethods(); i++ {
300+
m := typ.ExplicitMethod(i)
301+
if m.Exported() {
302+
methodNames = append(methodNames, m.Name())
303+
w.emitf("%s%s", m.Name(), w.signatureString(m.Type().(*types.Signature)))
304+
}
305+
}
306+
pop()
307+
308+
sort.Strings(methodNames)
309+
}
310+
311+
// emitf is the same as
312+
// https://go.googlesource.com/go/+/refs/tags/go1.16.6/src/cmd/api/goapi.go#997,
313+
// except verbose mode is removed.
314+
func (w *Walker) emitf(format string, args ...interface{}) {
315+
f := strings.Join(w.scope, ", ") + ", " + fmt.Sprintf(format, args...)
316+
if strings.Contains(f, "\n") {
317+
panic("feature contains newlines: " + f)
318+
}
319+
if _, dup := w.features[f]; dup {
320+
panic("duplicate feature inserted: " + f)
321+
}
322+
w.features[f] = true
323+
}
324+
325+
// goCmd is the same as
326+
// https://go.googlesource.com/go/+/refs/tags/go1.16.6/src/cmd/api/goapi.go#31,
327+
// except support for Windows is removed.
328+
func goCmd() string {
329+
path := filepath.Join(runtime.GOROOT(), "bin", "go")
330+
if _, err := os.Stat(path); err == nil {
331+
return path
332+
}
333+
return "go"
334+
}

0 commit comments

Comments
 (0)