| // Copyright 2011 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. | |
| // This package computes the exported API of a set of Go packages. | |
| // It is only a test, not a command, nor a usefully importable package. | |
| package main | |
| import ( | |
| "bufio" | |
| "bytes" | |
| "encoding/json" | |
| "fmt" | |
| "go/ast" | |
| "go/build" | |
| "go/parser" | |
| "go/token" | |
| "go/types" | |
| "internal/buildcfg" | |
| "internal/testenv" | |
| "io" | |
| "log" | |
| "os" | |
| "os/exec" | |
| "path/filepath" | |
| "regexp" | |
| "runtime" | |
| "slices" | |
| "strconv" | |
| "strings" | |
| "sync" | |
| "testing" | |
| ) | |
| const verbose = false | |
| func goCmd() string { | |
| var exeSuffix string | |
| if runtime.GOOS == "windows" { | |
| exeSuffix = ".exe" | |
| } | |
| path := filepath.Join(testenv.GOROOT(nil), "bin", "go"+exeSuffix) | |
| if _, err := os.Stat(path); err == nil { | |
| return path | |
| } | |
| return "go" | |
| } | |
| // contexts are the default contexts which are scanned. | |
| var contexts = []*build.Context{ | |
| {GOOS: "linux", GOARCH: "386", CgoEnabled: true}, | |
| {GOOS: "linux", GOARCH: "386"}, | |
| {GOOS: "linux", GOARCH: "amd64", CgoEnabled: true}, | |
| {GOOS: "linux", GOARCH: "amd64"}, | |
| {GOOS: "linux", GOARCH: "arm", CgoEnabled: true}, | |
| {GOOS: "linux", GOARCH: "arm"}, | |
| {GOOS: "darwin", GOARCH: "amd64", CgoEnabled: true}, | |
| {GOOS: "darwin", GOARCH: "amd64"}, | |
| {GOOS: "darwin", GOARCH: "arm64", CgoEnabled: true}, | |
| {GOOS: "darwin", GOARCH: "arm64"}, | |
| {GOOS: "windows", GOARCH: "amd64"}, | |
| {GOOS: "windows", GOARCH: "386"}, | |
| {GOOS: "freebsd", GOARCH: "386", CgoEnabled: true}, | |
| {GOOS: "freebsd", GOARCH: "386"}, | |
| {GOOS: "freebsd", GOARCH: "amd64", CgoEnabled: true}, | |
| {GOOS: "freebsd", GOARCH: "amd64"}, | |
| {GOOS: "freebsd", GOARCH: "arm", CgoEnabled: true}, | |
| {GOOS: "freebsd", GOARCH: "arm"}, | |
| {GOOS: "freebsd", GOARCH: "arm64", CgoEnabled: true}, | |
| {GOOS: "freebsd", GOARCH: "arm64"}, | |
| {GOOS: "freebsd", GOARCH: "riscv64", CgoEnabled: true}, | |
| {GOOS: "freebsd", GOARCH: "riscv64"}, | |
| {GOOS: "netbsd", GOARCH: "386", CgoEnabled: true}, | |
| {GOOS: "netbsd", GOARCH: "386"}, | |
| {GOOS: "netbsd", GOARCH: "amd64", CgoEnabled: true}, | |
| {GOOS: "netbsd", GOARCH: "amd64"}, | |
| {GOOS: "netbsd", GOARCH: "arm", CgoEnabled: true}, | |
| {GOOS: "netbsd", GOARCH: "arm"}, | |
| {GOOS: "netbsd", GOARCH: "arm64", CgoEnabled: true}, | |
| {GOOS: "netbsd", GOARCH: "arm64"}, | |
| {GOOS: "openbsd", GOARCH: "386", CgoEnabled: true}, | |
| {GOOS: "openbsd", GOARCH: "386"}, | |
| {GOOS: "openbsd", GOARCH: "amd64", CgoEnabled: true}, | |
| {GOOS: "openbsd", GOARCH: "amd64"}, | |
| } | |
| func contextName(c *build.Context) string { | |
| s := c.GOOS + "-" + c.GOARCH | |
| if c.CgoEnabled { | |
| s += "-cgo" | |
| } | |
| if c.Dir != "" { | |
| s += fmt.Sprintf(" [%s]", c.Dir) | |
| } | |
| return s | |
| } | |
| var internalPkg = regexp.MustCompile(`(^|/)internal($|/)`) | |
| var exitCode = 0 | |
| func Check(t *testing.T) { | |
| checkFiles, err := filepath.Glob(filepath.Join(testenv.GOROOT(t), "api/go1*.txt")) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| var nextFiles []string | |
| if v := runtime.Version(); strings.Contains(v, "devel") || strings.Contains(v, "beta") { | |
| next, err := filepath.Glob(filepath.Join(testenv.GOROOT(t), "api/next/*.txt")) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| nextFiles = next | |
| } | |
| for _, c := range contexts { | |
| c.Compiler = build.Default.Compiler | |
| // Include baseline goexperiment.* tool tags. | |
| baseline, err := buildcfg.ParseGOEXPERIMENT(c.GOOS, c.GOARCH, "") | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| for _, exp := range baseline.Enabled() { | |
| c.ToolTags = append(c.ToolTags, "goexperiment."+exp) | |
| } | |
| } | |
| walkers := make([]*Walker, len(contexts)) | |
| var wg sync.WaitGroup | |
| for i, context := range contexts { | |
| wg.Add(1) | |
| go func() { | |
| defer wg.Done() | |
| walkers[i] = NewWalker(context, filepath.Join(testenv.GOROOT(t), "src")) | |
| }() | |
| } | |
| wg.Wait() | |
| var featureCtx = make(map[string]map[string]bool) // feature -> context name -> true | |
| for _, w := range walkers { | |
| for _, name := range w.stdPackages { | |
| pkg, err := w.import_(name) | |
| if _, nogo := err.(*build.NoGoError); nogo { | |
| continue | |
| } | |
| if err != nil { | |
| log.Fatalf("Import(%q): %v", name, 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 | |
| } | |
| } | |
| var features []string | |
| for f, cmap := range featureCtx { | |
| if len(cmap) == len(contexts) { | |
| features = append(features, f) | |
| continue | |
| } | |
| comma := strings.Index(f, ",") | |
| for cname := range cmap { | |
| f2 := fmt.Sprintf("%s (%s)%s", f[:comma], cname, f[comma:]) | |
| features = append(features, f2) | |
| } | |
| } | |
| bw := bufio.NewWriter(os.Stdout) | |
| defer bw.Flush() | |
| var required []string | |
| for _, file := range checkFiles { | |
| required = append(required, fileFeatures(file, needApproval(file))...) | |
| } | |
| for _, file := range nextFiles { | |
| required = append(required, fileFeatures(file, true)...) | |
| } | |
| exception := fileFeatures(filepath.Join(testenv.GOROOT(t), "api/except.txt"), false) | |
| if exitCode == 1 { | |
| t.Errorf("API database problems found") | |
| } | |
| if !compareAPI(bw, features, required, exception) { | |
| t.Errorf("API differences found") | |
| } | |
| } | |
| // export emits the exported package features. | |
| func (w *Walker) export(pkg *apiPackage) { | |
| if verbose { | |
| log.Println(pkg) | |
| } | |
| pop := w.pushScope("pkg " + pkg.Path()) | |
| w.current = pkg | |
| w.collectDeprecated() | |
| scope := pkg.Scope() | |
| for _, name := range scope.Names() { | |
| if token.IsExported(name) { | |
| w.emitObj(scope.Lookup(name)) | |
| } | |
| } | |
| pop() | |
| } | |
| func set(items []string) map[string]bool { | |
| s := make(map[string]bool) | |
| for _, v := range items { | |
| s[v] = true | |
| } | |
| return s | |
| } | |
| var spaceParensRx = regexp.MustCompile(` \(\S+?\)`) | |
| func featureWithoutContext(f string) string { | |
| if !strings.Contains(f, "(") { | |
| return f | |
| } | |
| return spaceParensRx.ReplaceAllString(f, "") | |
| } | |
| // portRemoved reports whether the given port-specific API feature is | |
| // okay to no longer exist because its port was removed. | |
| func portRemoved(feature string) bool { | |
| return strings.Contains(feature, "(darwin-386)") || | |
| strings.Contains(feature, "(darwin-386-cgo)") | |
| } | |
| func compareAPI(w io.Writer, features, required, exception []string) (ok bool) { | |
| ok = true | |
| featureSet := set(features) | |
| exceptionSet := set(exception) | |
| slices.Sort(features) | |
| slices.Sort(required) | |
| take := func(sl *[]string) string { | |
| s := (*sl)[0] | |
| *sl = (*sl)[1:] | |
| return s | |
| } | |
| for len(features) > 0 || len(required) > 0 { | |
| switch { | |
| case len(features) == 0 || (len(required) > 0 && required[0] < features[0]): | |
| feature := take(&required) | |
| if exceptionSet[feature] { | |
| // An "unfortunate" case: the feature was once | |
| // included in the API (e.g. go1.txt), but was | |
| // subsequently removed. These are already | |
| // acknowledged by being in the file | |
| // "api/except.txt". No need to print them out | |
| // here. | |
| } else if portRemoved(feature) { | |
| // okay. | |
| } else if featureSet[featureWithoutContext(feature)] { | |
| // okay. | |
| } else { | |
| fmt.Fprintf(w, "-%s\n", feature) | |
| ok = false // broke compatibility | |
| } | |
| case len(required) == 0 || (len(features) > 0 && required[0] > features[0]): | |
| newFeature := take(&features) | |
| fmt.Fprintf(w, "+%s\n", newFeature) | |
| ok = false // feature not in api/next/* | |
| default: | |
| take(&required) | |
| take(&features) | |
| } | |
| } | |
| return ok | |
| } | |
| // aliasReplacer applies type aliases to earlier API files, | |
| // to avoid misleading negative results. | |
| // This makes all the references to os.FileInfo in go1.txt | |
| // be read as if they said fs.FileInfo, since os.FileInfo is now an alias. | |
| // If there are many of these, we could do a more general solution, | |
| // but for now the replacer is fine. | |
| var aliasReplacer = strings.NewReplacer( | |
| "os.FileInfo", "fs.FileInfo", | |
| "os.FileMode", "fs.FileMode", | |
| "os.PathError", "fs.PathError", | |
| ) | |
| func fileFeatures(filename string, needApproval bool) []string { | |
| bs, err := os.ReadFile(filename) | |
| if err != nil { | |
| log.Fatal(err) | |
| } | |
| s := string(bs) | |
| // Diagnose common mistakes people make, | |
| // since there is no apifmt to format these files. | |
| // The missing final newline is important for the | |
| // final release step of cat next/*.txt >go1.X.txt. | |
| // If the files don't end in full lines, the concatenation goes awry. | |
| if strings.Contains(s, "\r") { | |
| log.Printf("%s: contains CRLFs", filename) | |
| exitCode = 1 | |
| } | |
| if filepath.Base(filename) == "go1.4.txt" { | |
| // No use for blank lines in api files, except go1.4.txt | |
| // used them in a reasonable way and we should let it be. | |
| } else if strings.HasPrefix(s, "\n") || strings.Contains(s, "\n\n") { | |
| log.Printf("%s: contains a blank line", filename) | |
| exitCode = 1 | |
| } | |
| if s == "" { | |
| log.Printf("%s: empty file", filename) | |
| exitCode = 1 | |
| } else if s[len(s)-1] != '\n' { | |
| log.Printf("%s: missing final newline", filename) | |
| exitCode = 1 | |
| } | |
| s = aliasReplacer.Replace(s) | |
| lines := strings.Split(s, "\n") | |
| var nonblank []string | |
| for i, line := range lines { | |
| line = strings.TrimSpace(line) | |
| if line == "" || strings.HasPrefix(line, "#") { | |
| continue | |
| } | |
| if needApproval { | |
| feature, approval, ok := strings.Cut(line, "#") | |
| if !ok { | |
| log.Printf("%s:%d: missing proposal approval\n", filename, i+1) | |
| exitCode = 1 | |
| } else { | |
| _, err := strconv.Atoi(approval) | |
| if err != nil { | |
| log.Printf("%s:%d: malformed proposal approval #%s\n", filename, i+1, approval) | |
| exitCode = 1 | |
| } | |
| } | |
| line = strings.TrimSpace(feature) | |
| } else { | |
| if strings.Contains(line, " #") { | |
| log.Printf("%s:%d: unexpected approval\n", filename, i+1) | |
| exitCode = 1 | |
| } | |
| } | |
| nonblank = append(nonblank, line) | |
| } | |
| return nonblank | |
| } | |
| var fset = token.NewFileSet() | |
| type Walker struct { | |
| context *build.Context | |
| root string | |
| scope []string | |
| current *apiPackage | |
| deprecated map[token.Pos]bool | |
| features map[string]bool // set | |
| imported map[string]*apiPackage // packages already imported | |
| stdPackages []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 | |
| } | |
| func NewWalker(context *build.Context, root string) *Walker { | |
| w := &Walker{ | |
| context: context, | |
| root: root, | |
| features: map[string]bool{}, | |
| imported: map[string]*apiPackage{"unsafe": &apiPackage{Package: types.Unsafe}}, | |
| } | |
| w.loadImports() | |
| return w | |
| } | |
| func (w *Walker) Features() (fs []string) { | |
| for f := range w.features { | |
| fs = append(fs, f) | |
| } | |
| slices.Sort(fs) | |
| return | |
| } | |
| var parsedFileCache = make(map[string]*ast.File) | |
| func (w *Walker) parseFile(dir, file string) (*ast.File, error) { | |
| filename := filepath.Join(dir, file) | |
| if f := parsedFileCache[filename]; f != nil { | |
| return f, nil | |
| } | |
| f, err := parser.ParseFile(fset, filename, nil, parser.ParseComments|parser.SkipObjectResolution) | |
| if err != nil { | |
| return nil, err | |
| } | |
| parsedFileCache[filename] = f | |
| return f, nil | |
| } | |
| // Disable before debugging non-obvious errors from the type-checker. | |
| const usePkgCache = true | |
| var ( | |
| pkgCache = map[string]*apiPackage{} // map tagKey to package | |
| pkgTags = map[string][]string{} // map import dir to list of relevant tags | |
| ) | |
| // tagKey returns the tag-based key to use in the pkgCache. | |
| // It is a comma-separated string; the first part is dir, the rest tags. | |
| // The satisfied tags are derived from context but only those that | |
| // matter (the ones listed in the tags argument plus GOOS and GOARCH) are used. | |
| // The tags list, which came from go/build's Package.AllTags, | |
| // is known to be sorted. | |
| func tagKey(dir string, context *build.Context, tags []string) string { | |
| ctags := map[string]bool{ | |
| context.GOOS: true, | |
| context.GOARCH: true, | |
| } | |
| if context.CgoEnabled { | |
| ctags["cgo"] = true | |
| } | |
| for _, tag := range context.BuildTags { | |
| ctags[tag] = true | |
| } | |
| // TODO: ReleaseTags (need to load default) | |
| key := dir | |
| // explicit on GOOS and GOARCH as global cache will use "all" cached packages for | |
| // an indirect imported package. See https://github.com/golang/go/issues/21181 | |
| // for more detail. | |
| tags = append(tags, context.GOOS, context.GOARCH) | |
| slices.Sort(tags) | |
| for _, tag := range tags { | |
| if ctags[tag] { | |
| key += "," + tag | |
| ctags[tag] = false | |
| } | |
| } | |
| return key | |
| } | |
| type listImports struct { | |
| stdPackages []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 | |
| } | |
| var listCache sync.Map // map[string]listImports, keyed by contextName | |
| // listSem is a semaphore restricting concurrent invocations of 'go list'. 'go | |
| // list' has its own internal concurrency, so we use a hard-coded constant (to | |
| // allow the I/O-intensive phases of 'go list' to overlap) instead of scaling | |
| // all the way up to GOMAXPROCS. | |
| var listSem = make(chan semToken, 2) | |
| type semToken struct{} | |
| // 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() { | |
| if w.context == nil { | |
| return // test-only Walker; does not use the import map | |
| } | |
| name := contextName(w.context) | |
| imports, ok := listCache.Load(name) | |
| if !ok { | |
| listSem <- semToken{} | |
| defer func() { <-listSem }() | |
| cmd := exec.Command(goCmd(), "list", "-e", "-deps", "-json", "std") | |
| cmd.Env = listEnv(w.context) | |
| if w.context.Dir != "" { | |
| cmd.Dir = w.context.Dir | |
| } | |
| cmd.Stderr = os.Stderr | |
| out, err := cmd.Output() | |
| if err != nil { | |
| log.Fatalf("loading imports: %v\n%s", err, out) | |
| } | |
| var stdPackages []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("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.Standard && ip != "unsafe" && !strings.HasPrefix(ip, "vendor/") && !internalPkg.MatchString(ip) { | |
| stdPackages = append(stdPackages, 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 | |
| } | |
| } | |
| slices.Sort(stdPackages) | |
| imports = listImports{ | |
| stdPackages: stdPackages, | |
| importMap: importMap, | |
| importDir: importDir, | |
| } | |
| imports, _ = listCache.LoadOrStore(name, imports) | |
| } | |
| li := imports.(listImports) | |
| w.stdPackages = li.stdPackages | |
| w.importDir = li.importDir | |
| w.importMap = li.importMap | |
| } | |
| // listEnv returns the process environment to use when invoking 'go list' for | |
| // the given context. | |
| func listEnv(c *build.Context) []string { | |
| if c == nil { | |
| return os.Environ() | |
| } | |
| environ := append(os.Environ(), | |
| "GOOS="+c.GOOS, | |
| "GOARCH="+c.GOARCH) | |
| if c.CgoEnabled { | |
| environ = append(environ, "CGO_ENABLED=1") | |
| } else { | |
| environ = append(environ, "CGO_ENABLED=0") | |
| } | |
| return environ | |
| } | |
| type apiPackage struct { | |
| *types.Package | |
| Files []*ast.File | |
| } | |
| // Importing is a sentinel taking the place in Walker.imported | |
| // for a package that is in the process of being imported. | |
| var importing apiPackage | |
| // Import implements types.Importer. | |
| func (w *Walker) Import(name string) (*types.Package, error) { | |
| return w.ImportFrom(name, "", 0) | |
| } | |
| // ImportFrom implements types.ImporterFrom. | |
| func (w *Walker) ImportFrom(fromPath, fromDir string, mode types.ImportMode) (*types.Package, error) { | |
| pkg, err := w.importFrom(fromPath, fromDir, mode) | |
| if err != nil { | |
| return nil, err | |
| } | |
| return pkg.Package, nil | |
| } | |
| func (w *Walker) import_(name string) (*apiPackage, error) { | |
| return w.importFrom(name, "", 0) | |
| } | |
| func (w *Walker) importFrom(fromPath, fromDir string, mode types.ImportMode) (*apiPackage, error) { | |
| name := fromPath | |
| if canonical, ok := w.importMap[fromDir][fromPath]; ok { | |
| name = canonical | |
| } | |
| pkg := w.imported[name] | |
| if pkg != nil { | |
| if pkg == &importing { | |
| log.Fatalf("cycle importing package %q", name) | |
| } | |
| return pkg, nil | |
| } | |
| w.imported[name] = &importing | |
| // Determine package files. | |
| dir := w.importDir[name] | |
| if dir == "" { | |
| dir = filepath.Join(w.root, filepath.FromSlash(name)) | |
| } | |
| if fi, err := os.Stat(dir); err != nil || !fi.IsDir() { | |
| log.Panicf("no source in tree for import %q (from import %s in %s): %v", name, fromPath, fromDir, err) | |
| } | |
| context := w.context | |
| if context == nil { | |
| context = &build.Default | |
| } | |
| // Look in cache. | |
| // If we've already done an import with the same set | |
| // of relevant tags, reuse the result. | |
| var key string | |
| if usePkgCache { | |
| if tags, ok := pkgTags[dir]; ok { | |
| key = tagKey(dir, context, tags) | |
| if pkg := pkgCache[key]; pkg != nil { | |
| w.imported[name] = pkg | |
| return pkg, nil | |
| } | |
| } | |
| } | |
| info, err := context.ImportDir(dir, 0) | |
| if err != nil { | |
| if _, nogo := err.(*build.NoGoError); nogo { | |
| return nil, err | |
| } | |
| log.Fatalf("pkg %q, dir %q: ScanDir: %v", name, dir, err) | |
| } | |
| // Save tags list first time we see a directory. | |
| if usePkgCache { | |
| if _, ok := pkgTags[dir]; !ok { | |
| pkgTags[dir] = info.AllTags | |
| key = tagKey(dir, context, info.AllTags) | |
| } | |
| } | |
| filenames := append(append([]string{}, info.GoFiles...), info.CgoFiles...) | |
| // Parse package files. | |
| var files []*ast.File | |
| for _, file := range filenames { | |
| f, err := w.parseFile(dir, file) | |
| if err != nil { | |
| log.Fatalf("error parsing package %s: %s", name, err) | |
| } | |
| files = append(files, f) | |
| } | |
| // Type-check package files. | |
| var sizes types.Sizes | |
| if w.context != nil { | |
| sizes = types.SizesFor(w.context.Compiler, w.context.GOARCH) | |
| } | |
| conf := types.Config{ | |
| IgnoreFuncBodies: true, | |
| FakeImportC: true, | |
| Importer: w, | |
| Sizes: sizes, | |
| } | |
| tpkg, err := conf.Check(name, fset, files, nil) | |
| if err != nil { | |
| ctxt := "<no context>" | |
| if w.context != nil { | |
| ctxt = fmt.Sprintf("%s-%s", w.context.GOOS, w.context.GOARCH) | |
| } | |
| log.Fatalf("error typechecking package %s: %s (%s)", name, err, ctxt) | |
| } | |
| pkg = &apiPackage{tpkg, files} | |
| if usePkgCache { | |
| pkgCache[key] = pkg | |
| } | |
| w.imported[name] = pkg | |
| return pkg, nil | |
| } | |
| // pushScope enters a new scope (walking a package, type, node, etc) | |
| // and returns a function that will leave the scope (with sanity checking | |
| // for mismatched pushes & pops) | |
| func (w *Walker) pushScope(name string) (popFunc func()) { | |
| w.scope = append(w.scope, name) | |
| return func() { | |
| if len(w.scope) == 0 { | |
| log.Fatalf("attempt to leave scope %q with empty scope list", name) | |
| } | |
| if w.scope[len(w.scope)-1] != name { | |
| log.Fatalf("attempt to leave scope %q, but scope is currently %#v", name, w.scope) | |
| } | |
| w.scope = w.scope[:len(w.scope)-1] | |
| } | |
| } | |
| func sortedMethodNames(typ *types.Interface) []string { | |
| n := typ.NumMethods() | |
| list := make([]string, n) | |
| for i := range list { | |
| list[i] = typ.Method(i).Name() | |
| } | |
| slices.Sort(list) | |
| return list | |
| } | |
| // sortedEmbeddeds returns constraint types embedded in an | |
| // interface. It does not include embedded interface types or methods. | |
| func (w *Walker) sortedEmbeddeds(typ *types.Interface) []string { | |
| n := typ.NumEmbeddeds() | |
| list := make([]string, 0, n) | |
| for i := 0; i < n; i++ { | |
| emb := typ.EmbeddedType(i) | |
| switch emb := emb.(type) { | |
| case *types.Interface: | |
| list = append(list, w.sortedEmbeddeds(emb)...) | |
| case *types.Union: | |
| var buf bytes.Buffer | |
| nu := emb.Len() | |
| for i := 0; i < nu; i++ { | |
| if i > 0 { | |
| buf.WriteString(" | ") | |
| } | |
| term := emb.Term(i) | |
| if term.Tilde() { | |
| buf.WriteByte('~') | |
| } | |
| w.writeType(&buf, term.Type()) | |
| } | |
| list = append(list, buf.String()) | |
| } | |
| } | |
| slices.Sort(list) | |
| return list | |
| } | |
| func (w *Walker) writeType(buf *bytes.Buffer, typ types.Type) { | |
| switch typ := typ.(type) { | |
| case *types.Basic: | |
| s := typ.Name() | |
| switch typ.Kind() { | |
| case types.UnsafePointer: | |
| s = "unsafe.Pointer" | |
| case types.UntypedBool: | |
| s = "ideal-bool" | |
| case types.UntypedInt: | |
| s = "ideal-int" | |
| case types.UntypedRune: | |
| // "ideal-char" for compatibility with old tool | |
| // TODO(gri) change to "ideal-rune" | |
| s = "ideal-char" | |
| case types.UntypedFloat: | |
| s = "ideal-float" | |
| case types.UntypedComplex: | |
| s = "ideal-complex" | |
| case types.UntypedString: | |
| s = "ideal-string" | |
| case types.UntypedNil: | |
| panic("should never see untyped nil type") | |
| default: | |
| switch s { | |
| case "byte": | |
| s = "uint8" | |
| case "rune": | |
| s = "int32" | |
| } | |
| } | |
| buf.WriteString(s) | |
| case *types.Array: | |
| fmt.Fprintf(buf, "[%d]", typ.Len()) | |
| w.writeType(buf, typ.Elem()) | |
| case *types.Slice: | |
| buf.WriteString("[]") | |
| w.writeType(buf, typ.Elem()) | |
| case *types.Struct: | |
| buf.WriteString("struct") | |
| case *types.Pointer: | |
| buf.WriteByte('*') | |
| w.writeType(buf, typ.Elem()) | |
| case *types.Tuple: | |
| panic("should never see a tuple type") | |
| case *types.Signature: | |
| buf.WriteString("func") | |
| w.writeSignature(buf, typ) | |
| case *types.Interface: | |
| buf.WriteString("interface{") | |
| if typ.NumMethods() > 0 || typ.NumEmbeddeds() > 0 { | |
| buf.WriteByte(' ') | |
| } | |
| if typ.NumMethods() > 0 { | |
| buf.WriteString(strings.Join(sortedMethodNames(typ), ", ")) | |
| } | |
| if typ.NumEmbeddeds() > 0 { | |
| buf.WriteString(strings.Join(w.sortedEmbeddeds(typ), ", ")) | |
| } | |
| if typ.NumMethods() > 0 || typ.NumEmbeddeds() > 0 { | |
| buf.WriteByte(' ') | |
| } | |
| buf.WriteString("}") | |
| case *types.Map: | |
| buf.WriteString("map[") | |
| w.writeType(buf, typ.Key()) | |
| buf.WriteByte(']') | |
| w.writeType(buf, typ.Elem()) | |
| case *types.Chan: | |
| var s string | |
| switch typ.Dir() { | |
| case types.SendOnly: | |
| s = "chan<- " | |
| case types.RecvOnly: | |
| s = "<-chan " | |
| case types.SendRecv: | |
| s = "chan " | |
| default: | |
| panic("unreachable") | |
| } | |
| buf.WriteString(s) | |
| w.writeType(buf, typ.Elem()) | |
| case *types.Alias: | |
| w.writeType(buf, types.Unalias(typ)) | |
| case *types.Named: | |
| obj := typ.Obj() | |
| pkg := obj.Pkg() | |
| if pkg != nil && pkg != w.current.Package { | |
| buf.WriteString(pkg.Name()) | |
| buf.WriteByte('.') | |
| } | |
| buf.WriteString(typ.Obj().Name()) | |
| if targs := typ.TypeArgs(); targs.Len() > 0 { | |
| buf.WriteByte('[') | |
| for i := 0; i < targs.Len(); i++ { | |
| if i > 0 { | |
| buf.WriteString(", ") | |
| } | |
| w.writeType(buf, targs.At(i)) | |
| } | |
| buf.WriteByte(']') | |
| } | |
| case *types.TypeParam: | |
| // Type parameter names may change, so use a placeholder instead. | |
| fmt.Fprintf(buf, "$%d", typ.Index()) | |
| default: | |
| panic(fmt.Sprintf("unknown type %T", typ)) | |
| } | |
| } | |
| func (w *Walker) writeSignature(buf *bytes.Buffer, sig *types.Signature) { | |
| if tparams := sig.TypeParams(); tparams != nil { | |
| w.writeTypeParams(buf, tparams, true) | |
| } | |
| w.writeParams(buf, sig.Params(), sig.Variadic()) | |
| switch res := sig.Results(); res.Len() { | |
| case 0: | |
| // nothing to do | |
| case 1: | |
| buf.WriteByte(' ') | |
| w.writeType(buf, res.At(0).Type()) | |
| default: | |
| buf.WriteByte(' ') | |
| w.writeParams(buf, res, false) | |
| } | |
| } | |
| func (w *Walker) writeTypeParams(buf *bytes.Buffer, tparams *types.TypeParamList, withConstraints bool) { | |
| buf.WriteByte('[') | |
| c := tparams.Len() | |
| for i := 0; i < c; i++ { | |
| if i > 0 { | |
| buf.WriteString(", ") | |
| } | |
| tp := tparams.At(i) | |
| w.writeType(buf, tp) | |
| if withConstraints { | |
| buf.WriteByte(' ') | |
| w.writeType(buf, tp.Constraint()) | |
| } | |
| } | |
| buf.WriteByte(']') | |
| } | |
| func (w *Walker) writeParams(buf *bytes.Buffer, t *types.Tuple, variadic bool) { | |
| buf.WriteByte('(') | |
| for i, n := 0, t.Len(); i < n; i++ { | |
| if i > 0 { | |
| buf.WriteString(", ") | |
| } | |
| typ := t.At(i).Type() | |
| if variadic && i+1 == n { | |
| buf.WriteString("...") | |
| typ = typ.(*types.Slice).Elem() | |
| } | |
| w.writeType(buf, typ) | |
| } | |
| buf.WriteByte(')') | |
| } | |
| func (w *Walker) typeString(typ types.Type) string { | |
| var buf bytes.Buffer | |
| w.writeType(&buf, typ) | |
| return buf.String() | |
| } | |
| func (w *Walker) signatureString(sig *types.Signature) string { | |
| var buf bytes.Buffer | |
| w.writeSignature(&buf, sig) | |
| return buf.String() | |
| } | |
| func (w *Walker) emitObj(obj types.Object) { | |
| switch obj := obj.(type) { | |
| case *types.Const: | |
| if w.isDeprecated(obj) { | |
| w.emitf("const %s //deprecated", obj.Name()) | |
| } | |
| w.emitf("const %s %s", obj.Name(), w.typeString(obj.Type())) | |
| x := obj.Val() | |
| short := x.String() | |
| exact := x.ExactString() | |
| if short == exact { | |
| w.emitf("const %s = %s", obj.Name(), short) | |
| } else { | |
| w.emitf("const %s = %s // %s", obj.Name(), short, exact) | |
| } | |
| case *types.Var: | |
| if w.isDeprecated(obj) { | |
| w.emitf("var %s //deprecated", obj.Name()) | |
| } | |
| w.emitf("var %s %s", obj.Name(), w.typeString(obj.Type())) | |
| case *types.TypeName: | |
| w.emitType(obj) | |
| case *types.Func: | |
| w.emitFunc(obj) | |
| default: | |
| panic("unknown object: " + obj.String()) | |
| } | |
| } | |
| func (w *Walker) emitType(obj *types.TypeName) { | |
| name := obj.Name() | |
| if w.isDeprecated(obj) { | |
| w.emitf("type %s //deprecated", name) | |
| } | |
| typ := obj.Type() | |
| if obj.IsAlias() { | |
| w.emitf("type %s = %s", name, w.typeString(typ)) | |
| return | |
| } | |
| if tparams := obj.Type().(*types.Named).TypeParams(); tparams != nil { | |
| var buf bytes.Buffer | |
| buf.WriteString(name) | |
| w.writeTypeParams(&buf, tparams, true) | |
| name = buf.String() | |
| } | |
| switch typ := typ.Underlying().(type) { | |
| case *types.Struct: | |
| w.emitStructType(name, typ) | |
| case *types.Interface: | |
| w.emitIfaceType(name, typ) | |
| return // methods are handled by emitIfaceType | |
| default: | |
| w.emitf("type %s %s", name, w.typeString(typ.Underlying())) | |
| } | |
| // emit methods with value receiver | |
| var methodNames map[string]bool | |
| vset := types.NewMethodSet(typ) | |
| for i, n := 0, vset.Len(); i < n; i++ { | |
| m := vset.At(i) | |
| if m.Obj().Exported() { | |
| w.emitMethod(m) | |
| if methodNames == nil { | |
| methodNames = make(map[string]bool) | |
| } | |
| methodNames[m.Obj().Name()] = true | |
| } | |
| } | |
| // emit methods with pointer receiver; exclude | |
| // methods that we have emitted already | |
| // (the method set of *T includes the methods of T) | |
| pset := types.NewMethodSet(types.NewPointer(typ)) | |
| for i, n := 0, pset.Len(); i < n; i++ { | |
| m := pset.At(i) | |
| if m.Obj().Exported() && !methodNames[m.Obj().Name()] { | |
| w.emitMethod(m) | |
| } | |
| } | |
| } | |
| func (w *Walker) emitStructType(name string, typ *types.Struct) { | |
| typeStruct := fmt.Sprintf("type %s struct", name) | |
| w.emitf("%s", typeStruct) | |
| defer w.pushScope(typeStruct)() | |
| for i := 0; i < typ.NumFields(); i++ { | |
| f := typ.Field(i) | |
| if !f.Exported() { | |
| continue | |
| } | |
| typ := f.Type() | |
| if f.Anonymous() { | |
| if w.isDeprecated(f) { | |
| w.emitf("embedded %s //deprecated", w.typeString(typ)) | |
| } | |
| w.emitf("embedded %s", w.typeString(typ)) | |
| continue | |
| } | |
| if w.isDeprecated(f) { | |
| w.emitf("%s //deprecated", f.Name()) | |
| } | |
| w.emitf("%s %s", f.Name(), w.typeString(typ)) | |
| } | |
| } | |
| func (w *Walker) emitIfaceType(name string, typ *types.Interface) { | |
| pop := w.pushScope("type " + name + " interface") | |
| var methodNames []string | |
| complete := true | |
| mset := types.NewMethodSet(typ) | |
| for i, n := 0, mset.Len(); i < n; i++ { | |
| m := mset.At(i).Obj().(*types.Func) | |
| if !m.Exported() { | |
| complete = false | |
| continue | |
| } | |
| methodNames = append(methodNames, m.Name()) | |
| if w.isDeprecated(m) { | |
| w.emitf("%s //deprecated", m.Name()) | |
| } | |
| w.emitf("%s%s", m.Name(), w.signatureString(m.Signature())) | |
| } | |
| if !complete { | |
| // The method set has unexported methods, so all the | |
| // implementations are provided by the same package, | |
| // so the method set can be extended. Instead of recording | |
| // the full set of names (below), record only that there were | |
| // unexported methods. (If the interface shrinks, we will notice | |
| // because a method signature emitted during the last loop | |
| // will disappear.) | |
| w.emitf("unexported methods") | |
| } | |
| pop() | |
| if !complete { | |
| return | |
| } | |
| if len(methodNames) == 0 { | |
| w.emitf("type %s interface {}", name) | |
| return | |
| } | |
| slices.Sort(methodNames) | |
| w.emitf("type %s interface { %s }", name, strings.Join(methodNames, ", ")) | |
| } | |
| func (w *Walker) emitFunc(f *types.Func) { | |
| sig := f.Signature() | |
| if sig.Recv() != nil { | |
| panic("method considered a regular function: " + f.String()) | |
| } | |
| if w.isDeprecated(f) { | |
| w.emitf("func %s //deprecated", f.Name()) | |
| } | |
| w.emitf("func %s%s", f.Name(), w.signatureString(sig)) | |
| } | |
| func (w *Walker) emitMethod(m *types.Selection) { | |
| sig := m.Type().(*types.Signature) | |
| recv := sig.Recv().Type() | |
| // report exported methods with unexported receiver base type | |
| if true { | |
| base := recv | |
| if p, _ := recv.(*types.Pointer); p != nil { | |
| base = p.Elem() | |
| } | |
| if obj := base.(*types.Named).Obj(); !obj.Exported() { | |
| log.Fatalf("exported method with unexported receiver base type: %s", m) | |
| } | |
| } | |
| tps := "" | |
| if rtp := sig.RecvTypeParams(); rtp != nil { | |
| var buf bytes.Buffer | |
| w.writeTypeParams(&buf, rtp, false) | |
| tps = buf.String() | |
| } | |
| if w.isDeprecated(m.Obj()) { | |
| w.emitf("method (%s%s) %s //deprecated", w.typeString(recv), tps, m.Obj().Name()) | |
| } | |
| w.emitf("method (%s%s) %s%s", w.typeString(recv), tps, m.Obj().Name(), w.signatureString(sig)) | |
| } | |
| 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 | |
| if verbose { | |
| log.Printf("feature: %s", f) | |
| } | |
| } | |
| func needApproval(filename string) bool { | |
| name := filepath.Base(filename) | |
| if name == "go1.txt" { | |
| return false | |
| } | |
| minor := strings.TrimSuffix(strings.TrimPrefix(name, "go1."), ".txt") | |
| n, err := strconv.Atoi(minor) | |
| if err != nil { | |
| log.Fatalf("unexpected api file: %v", name) | |
| } | |
| return n >= 19 // started tracking approvals in Go 1.19 | |
| } | |
| func (w *Walker) collectDeprecated() { | |
| isDeprecated := func(doc *ast.CommentGroup) bool { | |
| // Look for "Deprecated:" (case-sensitive) at the beginning (not middle) of a paragraph. | |
| // It's typically found in the last paragraph, but it's not required to be the last one. | |
| // The colon is typically followed by a space, but it can also be a newline, as was the | |
| // case at https://go.dev/pkg/go/build#AllowBinary for example. | |
| // | |
| // See https://go.dev/wiki/Deprecated and https://go.dev/ref/mod#go-mod-file-module-deprecation. | |
| text := doc.Text() | |
| return strings.HasPrefix(text, "Deprecated: ") || strings.Contains(text, "\n\nDeprecated: ") || | |
| strings.HasPrefix(text, "Deprecated:\n") || strings.Contains(text, "\n\nDeprecated:\n") | |
| } | |
| w.deprecated = make(map[token.Pos]bool) | |
| mark := func(id *ast.Ident) { | |
| if id != nil { | |
| w.deprecated[id.Pos()] = true | |
| } | |
| } | |
| for _, file := range w.current.Files { | |
| ast.Inspect(file, func(n ast.Node) bool { | |
| switch n := n.(type) { | |
| case *ast.File: | |
| if isDeprecated(n.Doc) { | |
| mark(n.Name) | |
| } | |
| return true | |
| case *ast.GenDecl: | |
| if isDeprecated(n.Doc) { | |
| for _, spec := range n.Specs { | |
| switch spec := spec.(type) { | |
| case *ast.ValueSpec: | |
| for _, id := range spec.Names { | |
| mark(id) | |
| } | |
| case *ast.TypeSpec: | |
| mark(spec.Name) | |
| } | |
| } | |
| } | |
| return true // look at specs | |
| case *ast.FuncDecl: | |
| if isDeprecated(n.Doc) { | |
| mark(n.Name) | |
| } | |
| return false | |
| case *ast.TypeSpec: | |
| if isDeprecated(n.Doc) { | |
| mark(n.Name) | |
| } | |
| return true // recurse into struct or interface type | |
| case *ast.StructType: | |
| return true // recurse into fields | |
| case *ast.InterfaceType: | |
| return true // recurse into methods | |
| case *ast.FieldList: | |
| return true // recurse into fields | |
| case *ast.ValueSpec: | |
| if isDeprecated(n.Doc) { | |
| for _, id := range n.Names { | |
| mark(id) | |
| } | |
| } | |
| return false | |
| case *ast.Field: | |
| if isDeprecated(n.Doc) { | |
| for _, id := range n.Names { | |
| mark(id) | |
| } | |
| if len(n.Names) == 0 { | |
| // embedded field T or *T? | |
| typ := n.Type | |
| if ptr, ok := typ.(*ast.StarExpr); ok { | |
| typ = ptr.X | |
| } | |
| if id, ok := typ.(*ast.Ident); ok { | |
| mark(id) | |
| } | |
| } | |
| } | |
| return false | |
| default: | |
| return false | |
| } | |
| }) | |
| } | |
| } | |
| func (w *Walker) isDeprecated(obj types.Object) bool { | |
| return w.deprecated[obj.Pos()] | |
| } |