|
| 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