1// Copyright 2011 The Go Authors. All rights reserved.2// Use of this source code is governed by a BSD-style3// license that can be found in the LICENSE file.45// This package computes the exported API of a set of Go packages.6// It is only a test, not a command, nor a usefully importable package.78package main910import (11 "bufio"12 "bytes"13 "encoding/json"14 "fmt"15 "go/ast"16 "go/build"17 "go/parser"18 "go/token"19 "go/types"20 "internal/buildcfg"21 "internal/testenv"22 "io"23 "log"24 "os"25 "os/exec"26 "path/filepath"27 "regexp"28 "runtime"29 "slices"30 "strconv"31 "strings"32 "sync"33 "testing"34)3536const verbose = false3738func goCmd() string {39 var exeSuffix string40 if runtime.GOOS == "windows" {41 exeSuffix = ".exe"42 }43 path := filepath.Join(testenv.GOROOT(nil), "bin", "go"+exeSuffix)44 if _, err := os.Stat(path); err == nil {45 return path46 }47 return "go"48}4950// contexts are the default contexts which are scanned.51var contexts = []*build.Context{52 {GOOS: "linux", GOARCH: "386", CgoEnabled: true},53 {GOOS: "linux", GOARCH: "386"},54 {GOOS: "linux", GOARCH: "amd64", CgoEnabled: true},55 {GOOS: "linux", GOARCH: "amd64"},56 {GOOS: "linux", GOARCH: "arm", CgoEnabled: true},57 {GOOS: "linux", GOARCH: "arm"},58 {GOOS: "darwin", GOARCH: "amd64", CgoEnabled: true},59 {GOOS: "darwin", GOARCH: "amd64"},60 {GOOS: "darwin", GOARCH: "arm64", CgoEnabled: true},61 {GOOS: "darwin", GOARCH: "arm64"},62 {GOOS: "windows", GOARCH: "amd64"},63 {GOOS: "windows", GOARCH: "386"},64 {GOOS: "freebsd", GOARCH: "386", CgoEnabled: true},65 {GOOS: "freebsd", GOARCH: "386"},66 {GOOS: "freebsd", GOARCH: "amd64", CgoEnabled: true},67 {GOOS: "freebsd", GOARCH: "amd64"},68 {GOOS: "freebsd", GOARCH: "arm", CgoEnabled: true},69 {GOOS: "freebsd", GOARCH: "arm"},70 {GOOS: "freebsd", GOARCH: "arm64", CgoEnabled: true},71 {GOOS: "freebsd", GOARCH: "arm64"},72 {GOOS: "freebsd", GOARCH: "riscv64", CgoEnabled: true},73 {GOOS: "freebsd", GOARCH: "riscv64"},74 {GOOS: "netbsd", GOARCH: "386", CgoEnabled: true},75 {GOOS: "netbsd", GOARCH: "386"},76 {GOOS: "netbsd", GOARCH: "amd64", CgoEnabled: true},77 {GOOS: "netbsd", GOARCH: "amd64"},78 {GOOS: "netbsd", GOARCH: "arm", CgoEnabled: true},79 {GOOS: "netbsd", GOARCH: "arm"},80 {GOOS: "netbsd", GOARCH: "arm64", CgoEnabled: true},81 {GOOS: "netbsd", GOARCH: "arm64"},82 {GOOS: "openbsd", GOARCH: "386", CgoEnabled: true},83 {GOOS: "openbsd", GOARCH: "386"},84 {GOOS: "openbsd", GOARCH: "amd64", CgoEnabled: true},85 {GOOS: "openbsd", GOARCH: "amd64"},86}8788func contextName(c *build.Context) string {89 s := c.GOOS + "-" + c.GOARCH90 if c.CgoEnabled {91 s += "-cgo"92 }93 if c.Dir != "" {94 s += fmt.Sprintf(" [%s]", c.Dir)95 }96 return s97}9899var internalPkg = regexp.MustCompile(`(^|/)internal($|/)`)100101var exitCode = 0102103func Check(t *testing.T) {104 checkFiles, err := filepath.Glob(filepath.Join(testenv.GOROOT(t), "api/go1*.txt"))105 if err != nil {106 t.Fatal(err)107 }108109 var nextFiles []string110 if v := runtime.Version(); strings.Contains(v, "devel") || strings.Contains(v, "beta") {111 next, err := filepath.Glob(filepath.Join(testenv.GOROOT(t), "api/next/*.txt"))112 if err != nil {113 t.Fatal(err)114 }115 nextFiles = next116 }117118 for _, c := range contexts {119 c.Compiler = build.Default.Compiler120121 // Include baseline goexperiment.* tool tags.122 baseline, err := buildcfg.ParseGOEXPERIMENT(c.GOOS, c.GOARCH, "")123 if err != nil {124 t.Fatal(err)125 }126 for _, exp := range baseline.Enabled() {127 c.ToolTags = append(c.ToolTags, "goexperiment."+exp)128 }129 }130131 walkers := make([]*Walker, len(contexts))132 var wg sync.WaitGroup133 for i, context := range contexts {134 wg.Add(1)135 go func() {136 defer wg.Done()137 walkers[i] = NewWalker(context, filepath.Join(testenv.GOROOT(t), "src"))138 }()139 }140 wg.Wait()141142 var featureCtx = make(map[string]map[string]bool) // feature -> context name -> true143 for _, w := range walkers {144 for _, name := range w.stdPackages {145 pkg, err := w.import_(name)146 if _, nogo := err.(*build.NoGoError); nogo {147 continue148 }149 if err != nil {150 log.Fatalf("Import(%q): %v", name, err)151 }152 w.export(pkg)153 }154155 ctxName := contextName(w.context)156 for _, f := range w.Features() {157 if featureCtx[f] == nil {158 featureCtx[f] = make(map[string]bool)159 }160 featureCtx[f][ctxName] = true161 }162 }163164 var features []string165 for f, cmap := range featureCtx {166 if len(cmap) == len(contexts) {167 features = append(features, f)168 continue169 }170 comma := strings.Index(f, ",")171 for cname := range cmap {172 f2 := fmt.Sprintf("%s (%s)%s", f[:comma], cname, f[comma:])173 features = append(features, f2)174 }175 }176177 bw := bufio.NewWriter(os.Stdout)178 defer bw.Flush()179180 var required []string181 for _, file := range checkFiles {182 required = append(required, fileFeatures(file, needApproval(file))...)183 }184 for _, file := range nextFiles {185 required = append(required, fileFeatures(file, true)...)186 }187 exception := fileFeatures(filepath.Join(testenv.GOROOT(t), "api/except.txt"), false)188189 if exitCode == 1 {190 t.Errorf("API database problems found")191 }192 if !compareAPI(bw, features, required, exception) {193 t.Errorf("API differences found")194 }195}196197// export emits the exported package features.198func (w *Walker) export(pkg *apiPackage) {199 if verbose {200 log.Println(pkg)201 }202 pop := w.pushScope("pkg " + pkg.Path())203 w.current = pkg204 w.collectDeprecated()205 scope := pkg.Scope()206 for _, name := range scope.Names() {207 if token.IsExported(name) {208 w.emitObj(scope.Lookup(name))209 }210 }211 pop()212}213214func set(items []string) map[string]bool {215 s := make(map[string]bool)216 for _, v := range items {217 s[v] = true218 }219 return s220}221222var spaceParensRx = regexp.MustCompile(` \(\S+?\)`)223224func featureWithoutContext(f string) string {225 if !strings.Contains(f, "(") {226 return f227 }228 return spaceParensRx.ReplaceAllString(f, "")229}230231// portRemoved reports whether the given port-specific API feature is232// okay to no longer exist because its port was removed.233func portRemoved(feature string) bool {234 return strings.Contains(feature, "(darwin-386)") ||235 strings.Contains(feature, "(darwin-386-cgo)")236}237238func compareAPI(w io.Writer, features, required, exception []string) (ok bool) {239 ok = true240241 featureSet := set(features)242 exceptionSet := set(exception)243244 slices.Sort(features)245 slices.Sort(required)246247 take := func(sl *[]string) string {248 s := (*sl)[0]249 *sl = (*sl)[1:]250 return s251 }252253 for len(features) > 0 || len(required) > 0 {254 switch {255 case len(features) == 0 || (len(required) > 0 && required[0] < features[0]):256 feature := take(&required)257 if exceptionSet[feature] {258 // An "unfortunate" case: the feature was once259 // included in the API (e.g. go1.txt), but was260 // subsequently removed. These are already261 // acknowledged by being in the file262 // "api/except.txt". No need to print them out263 // here.264 } else if portRemoved(feature) {265 // okay.266 } else if featureSet[featureWithoutContext(feature)] {267 // okay.268 } else {269 fmt.Fprintf(w, "-%s\n", feature)270 ok = false // broke compatibility271 }272 case len(required) == 0 || (len(features) > 0 && required[0] > features[0]):273 newFeature := take(&features)274 fmt.Fprintf(w, "+%s\n", newFeature)275 ok = false // feature not in api/next/*276 default:277 take(&required)278 take(&features)279 }280 }281282 return ok283}284285// aliasReplacer applies type aliases to earlier API files,286// to avoid misleading negative results.287// This makes all the references to os.FileInfo in go1.txt288// be read as if they said fs.FileInfo, since os.FileInfo is now an alias.289// If there are many of these, we could do a more general solution,290// but for now the replacer is fine.291var aliasReplacer = strings.NewReplacer(292 "os.FileInfo", "fs.FileInfo",293 "os.FileMode", "fs.FileMode",294 "os.PathError", "fs.PathError",295)296297func fileFeatures(filename string, needApproval bool) []string {298 bs, err := os.ReadFile(filename)299 if err != nil {300 log.Fatal(err)301 }302 s := string(bs)303304 // Diagnose common mistakes people make,305 // since there is no apifmt to format these files.306 // The missing final newline is important for the307 // final release step of cat next/*.txt >go1.X.txt.308 // If the files don't end in full lines, the concatenation goes awry.309 if strings.Contains(s, "\r") {310 log.Printf("%s: contains CRLFs", filename)311 exitCode = 1312 }313 if filepath.Base(filename) == "go1.4.txt" {314 // No use for blank lines in api files, except go1.4.txt315 // used them in a reasonable way and we should let it be.316 } else if strings.HasPrefix(s, "\n") || strings.Contains(s, "\n\n") {317 log.Printf("%s: contains a blank line", filename)318 exitCode = 1319 }320 if s == "" {321 log.Printf("%s: empty file", filename)322 exitCode = 1323 } else if s[len(s)-1] != '\n' {324 log.Printf("%s: missing final newline", filename)325 exitCode = 1326 }327 s = aliasReplacer.Replace(s)328 lines := strings.Split(s, "\n")329 var nonblank []string330 for i, line := range lines {331 line = strings.TrimSpace(line)332 if line == "" || strings.HasPrefix(line, "#") {333 continue334 }335 if needApproval {336 feature, approval, ok := strings.Cut(line, "#")337 if !ok {338 log.Printf("%s:%d: missing proposal approval\n", filename, i+1)339 exitCode = 1340 } else {341 _, err := strconv.Atoi(approval)342 if err != nil {343 log.Printf("%s:%d: malformed proposal approval #%s\n", filename, i+1, approval)344 exitCode = 1345 }346 }347 line = strings.TrimSpace(feature)348 } else {349 if strings.Contains(line, " #") {350 log.Printf("%s:%d: unexpected approval\n", filename, i+1)351 exitCode = 1352 }353 }354 nonblank = append(nonblank, line)355 }356 return nonblank357}358359var fset = token.NewFileSet()360361type Walker struct {362 context *build.Context363 root string364 scope []string365 current *apiPackage366 deprecated map[token.Pos]bool367 features map[string]bool // set368 imported map[string]*apiPackage // packages already imported369 stdPackages []string // names, omitting "unsafe", internal, and vendored packages370 importMap map[string]map[string]string // importer dir -> import path -> canonical path371 importDir map[string]string // canonical import path -> dir372373}374375func NewWalker(context *build.Context, root string) *Walker {376 w := &Walker{377 context: context,378 root: root,379 features: map[string]bool{},380 imported: map[string]*apiPackage{"unsafe": &apiPackage{Package: types.Unsafe}},381 }382 w.loadImports()383 return w384}385386func (w *Walker) Features() (fs []string) {387 for f := range w.features {388 fs = append(fs, f)389 }390 slices.Sort(fs)391 return392}393394var parsedFileCache = make(map[string]*ast.File)395396func (w *Walker) parseFile(dir, file string) (*ast.File, error) {397 filename := filepath.Join(dir, file)398 if f := parsedFileCache[filename]; f != nil {399 return f, nil400 }401402 f, err := parser.ParseFile(fset, filename, nil, parser.ParseComments|parser.SkipObjectResolution)403 if err != nil {404 return nil, err405 }406 parsedFileCache[filename] = f407408 return f, nil409}410411// Disable before debugging non-obvious errors from the type-checker.412const usePkgCache = true413414var (415 pkgCache = map[string]*apiPackage{} // map tagKey to package416 pkgTags = map[string][]string{} // map import dir to list of relevant tags417)418419// tagKey returns the tag-based key to use in the pkgCache.420// It is a comma-separated string; the first part is dir, the rest tags.421// The satisfied tags are derived from context but only those that422// matter (the ones listed in the tags argument plus GOOS and GOARCH) are used.423// The tags list, which came from go/build's Package.AllTags,424// is known to be sorted.425func tagKey(dir string, context *build.Context, tags []string) string {426 ctags := map[string]bool{427 context.GOOS: true,428 context.GOARCH: true,429 }430 if context.CgoEnabled {431 ctags["cgo"] = true432 }433 for _, tag := range context.BuildTags {434 ctags[tag] = true435 }436 // TODO: ReleaseTags (need to load default)437 key := dir438439 // explicit on GOOS and GOARCH as global cache will use "all" cached packages for440 // an indirect imported package. See https://github.com/golang/go/issues/21181441 // for more detail.442 tags = append(tags, context.GOOS, context.GOARCH)443 slices.Sort(tags)444445 for _, tag := range tags {446 if ctags[tag] {447 key += "," + tag448 ctags[tag] = false449 }450 }451 return key452}453454type listImports struct {455 stdPackages []string // names, omitting "unsafe", internal, and vendored packages456 importDir map[string]string // canonical import path → directory457 importMap map[string]map[string]string // import path → canonical import path458}459460var listCache sync.Map // map[string]listImports, keyed by contextName461462// listSem is a semaphore restricting concurrent invocations of 'go list'. 'go463// list' has its own internal concurrency, so we use a hard-coded constant (to464// allow the I/O-intensive phases of 'go list' to overlap) instead of scaling465// all the way up to GOMAXPROCS.466var listSem = make(chan semToken, 2)467468type semToken struct{}469470// loadImports populates w with information about the packages in the standard471// library and the packages they themselves import in w's build context.472//473// The source import path and expanded import path are identical except for vendored packages.474// For example, on return:475//476// w.importMap["math"] = "math"477// w.importDir["math"] = "<goroot>/src/math"478//479// w.importMap["golang.org/x/net/route"] = "vendor/golang.org/x/net/route"480// w.importDir["vendor/golang.org/x/net/route"] = "<goroot>/src/vendor/golang.org/x/net/route"481//482// Since the set of packages that exist depends on context, the result of483// loadImports also depends on context. However, to improve test running time484// the configuration for each environment is cached across runs.485func (w *Walker) loadImports() {486 if w.context == nil {487 return // test-only Walker; does not use the import map488 }489490 name := contextName(w.context)491492 imports, ok := listCache.Load(name)493 if !ok {494 listSem <- semToken{}495 defer func() { <-listSem }()496497 cmd := exec.Command(goCmd(), "list", "-e", "-deps", "-json", "std")498 cmd.Env = listEnv(w.context)499 if w.context.Dir != "" {500 cmd.Dir = w.context.Dir501 }502 cmd.Stderr = os.Stderr503 out, err := cmd.Output()504 if err != nil {505 log.Fatalf("loading imports: %v\n%s", err, out)506 }507508 var stdPackages []string509 importMap := make(map[string]map[string]string)510 importDir := make(map[string]string)511 dec := json.NewDecoder(bytes.NewReader(out))512 for {513 var pkg struct {514 ImportPath, Dir string515 ImportMap map[string]string516 Standard bool517 }518 err := dec.Decode(&pkg)519 if err == io.EOF {520 break521 }522 if err != nil {523 log.Fatalf("go list: invalid output: %v", err)524 }525526 // - Package "unsafe" contains special signatures requiring527 // extra care when printing them - ignore since it is not528 // going to change w/o a language change.529 // - Internal and vendored packages do not contribute to our530 // API surface. (If we are running within the "std" module,531 // vendored dependencies appear as themselves instead of532 // their "vendor/" standard-library copies.)533 // - 'go list std' does not include commands, which cannot be534 // imported anyway.535 if ip := pkg.ImportPath; pkg.Standard && ip != "unsafe" && !strings.HasPrefix(ip, "vendor/") && !internalPkg.MatchString(ip) {536 stdPackages = append(stdPackages, ip)537 }538 importDir[pkg.ImportPath] = pkg.Dir539 if len(pkg.ImportMap) > 0 {540 importMap[pkg.Dir] = make(map[string]string, len(pkg.ImportMap))541 }542 for k, v := range pkg.ImportMap {543 importMap[pkg.Dir][k] = v544 }545 }546547 slices.Sort(stdPackages)548 imports = listImports{549 stdPackages: stdPackages,550 importMap: importMap,551 importDir: importDir,552 }553 imports, _ = listCache.LoadOrStore(name, imports)554 }555556 li := imports.(listImports)557 w.stdPackages = li.stdPackages558 w.importDir = li.importDir559 w.importMap = li.importMap560}561562// listEnv returns the process environment to use when invoking 'go list' for563// the given context.564func listEnv(c *build.Context) []string {565 if c == nil {566 return os.Environ()567 }568569 environ := append(os.Environ(),570 "GOOS="+c.GOOS,571 "GOARCH="+c.GOARCH)572 if c.CgoEnabled {573 environ = append(environ, "CGO_ENABLED=1")574 } else {575 environ = append(environ, "CGO_ENABLED=0")576 }577 return environ578}579580type apiPackage struct {581 *types.Package582 Files []*ast.File583}584585// Importing is a sentinel taking the place in Walker.imported586// for a package that is in the process of being imported.587var importing apiPackage588589// Import implements types.Importer.590func (w *Walker) Import(name string) (*types.Package, error) {591 return w.ImportFrom(name, "", 0)592}593594// ImportFrom implements types.ImporterFrom.595func (w *Walker) ImportFrom(fromPath, fromDir string, mode types.ImportMode) (*types.Package, error) {596 pkg, err := w.importFrom(fromPath, fromDir, mode)597 if err != nil {598 return nil, err599 }600 return pkg.Package, nil601}602603func (w *Walker) import_(name string) (*apiPackage, error) {604 return w.importFrom(name, "", 0)605}606607func (w *Walker) importFrom(fromPath, fromDir string, mode types.ImportMode) (*apiPackage, error) {608 name := fromPath609 if canonical, ok := w.importMap[fromDir][fromPath]; ok {610 name = canonical611 }612613 pkg := w.imported[name]614 if pkg != nil {615 if pkg == &importing {616 log.Fatalf("cycle importing package %q", name)617 }618 return pkg, nil619 }620 w.imported[name] = &importing621622 // Determine package files.623 dir := w.importDir[name]624 if dir == "" {625 dir = filepath.Join(w.root, filepath.FromSlash(name))626 }627 if fi, err := os.Stat(dir); err != nil || !fi.IsDir() {628 log.Panicf("no source in tree for import %q (from import %s in %s): %v", name, fromPath, fromDir, err)629 }630631 context := w.context632 if context == nil {633 context = &build.Default634 }635636 // Look in cache.637 // If we've already done an import with the same set638 // of relevant tags, reuse the result.639 var key string640 if usePkgCache {641 if tags, ok := pkgTags[dir]; ok {642 key = tagKey(dir, context, tags)643 if pkg := pkgCache[key]; pkg != nil {644 w.imported[name] = pkg645 return pkg, nil646 }647 }648 }649650 info, err := context.ImportDir(dir, 0)651 if err != nil {652 if _, nogo := err.(*build.NoGoError); nogo {653 return nil, err654 }655 log.Fatalf("pkg %q, dir %q: ScanDir: %v", name, dir, err)656 }657658 // Save tags list first time we see a directory.659 if usePkgCache {660 if _, ok := pkgTags[dir]; !ok {661 pkgTags[dir] = info.AllTags662 key = tagKey(dir, context, info.AllTags)663 }664 }665666 filenames := append(append([]string{}, info.GoFiles...), info.CgoFiles...)667668 // Parse package files.669 var files []*ast.File670 for _, file := range filenames {671 f, err := w.parseFile(dir, file)672 if err != nil {673 log.Fatalf("error parsing package %s: %s", name, err)674 }675 files = append(files, f)676 }677678 // Type-check package files.679 var sizes types.Sizes680 if w.context != nil {681 sizes = types.SizesFor(w.context.Compiler, w.context.GOARCH)682 }683 conf := types.Config{684 IgnoreFuncBodies: true,685 FakeImportC: true,686 Importer: w,687 Sizes: sizes,688 }689 tpkg, err := conf.Check(name, fset, files, nil)690 if err != nil {691 ctxt := "<no context>"692 if w.context != nil {693 ctxt = fmt.Sprintf("%s-%s", w.context.GOOS, w.context.GOARCH)694 }695 log.Fatalf("error typechecking package %s: %s (%s)", name, err, ctxt)696 }697 pkg = &apiPackage{tpkg, files}698699 if usePkgCache {700 pkgCache[key] = pkg701 }702703 w.imported[name] = pkg704 return pkg, nil705}706707// pushScope enters a new scope (walking a package, type, node, etc)708// and returns a function that will leave the scope (with sanity checking709// for mismatched pushes & pops)710func (w *Walker) pushScope(name string) (popFunc func()) {711 w.scope = append(w.scope, name)712 return func() {713 if len(w.scope) == 0 {714 log.Fatalf("attempt to leave scope %q with empty scope list", name)715 }716 if w.scope[len(w.scope)-1] != name {717 log.Fatalf("attempt to leave scope %q, but scope is currently %#v", name, w.scope)718 }719 w.scope = w.scope[:len(w.scope)-1]720 }721}722723func sortedMethodNames(typ *types.Interface) []string {724 n := typ.NumMethods()725 list := make([]string, n)726 for i := range list {727 list[i] = typ.Method(i).Name()728 }729 slices.Sort(list)730 return list731}732733// sortedEmbeddeds returns constraint types embedded in an734// interface. It does not include embedded interface types or methods.735func (w *Walker) sortedEmbeddeds(typ *types.Interface) []string {736 n := typ.NumEmbeddeds()737 list := make([]string, 0, n)738 for i := 0; i < n; i++ {739 emb := typ.EmbeddedType(i)740 switch emb := emb.(type) {741 case *types.Interface:742 list = append(list, w.sortedEmbeddeds(emb)...)743 case *types.Union:744 var buf bytes.Buffer745 nu := emb.Len()746 for i := 0; i < nu; i++ {747 if i > 0 {748 buf.WriteString(" | ")749 }750 term := emb.Term(i)751 if term.Tilde() {752 buf.WriteByte('~')753 }754 w.writeType(&buf, term.Type())755 }756 list = append(list, buf.String())757 }758 }759 slices.Sort(list)760 return list761}762763func (w *Walker) writeType(buf *bytes.Buffer, typ types.Type) {764 switch typ := typ.(type) {765 case *types.Basic:766 s := typ.Name()767 switch typ.Kind() {768 case types.UnsafePointer:769 s = "unsafe.Pointer"770 case types.UntypedBool:771 s = "ideal-bool"772 case types.UntypedInt:773 s = "ideal-int"774 case types.UntypedRune:775 // "ideal-char" for compatibility with old tool776 // TODO(gri) change to "ideal-rune"777 s = "ideal-char"778 case types.UntypedFloat:779 s = "ideal-float"780 case types.UntypedComplex:781 s = "ideal-complex"782 case types.UntypedString:783 s = "ideal-string"784 case types.UntypedNil:785 panic("should never see untyped nil type")786 default:787 switch s {788 case "byte":789 s = "uint8"790 case "rune":791 s = "int32"792 }793 }794 buf.WriteString(s)795796 case *types.Array:797 fmt.Fprintf(buf, "[%d]", typ.Len())798 w.writeType(buf, typ.Elem())799800 case *types.Slice:801 buf.WriteString("[]")802 w.writeType(buf, typ.Elem())803804 case *types.Struct:805 buf.WriteString("struct")806807 case *types.Pointer:808 buf.WriteByte('*')809 w.writeType(buf, typ.Elem())810811 case *types.Tuple:812 panic("should never see a tuple type")813814 case *types.Signature:815 buf.WriteString("func")816 w.writeSignature(buf, typ)817818 case *types.Interface:819 buf.WriteString("interface{")820 if typ.NumMethods() > 0 || typ.NumEmbeddeds() > 0 {821 buf.WriteByte(' ')822 }823 if typ.NumMethods() > 0 {824 buf.WriteString(strings.Join(sortedMethodNames(typ), ", "))825 }826 if typ.NumEmbeddeds() > 0 {827 buf.WriteString(strings.Join(w.sortedEmbeddeds(typ), ", "))828 }829 if typ.NumMethods() > 0 || typ.NumEmbeddeds() > 0 {830 buf.WriteByte(' ')831 }832 buf.WriteString("}")833834 case *types.Map:835 buf.WriteString("map[")836 w.writeType(buf, typ.Key())837 buf.WriteByte(']')838 w.writeType(buf, typ.Elem())839840 case *types.Chan:841 var s string842 switch typ.Dir() {843 case types.SendOnly:844 s = "chan<- "845 case types.RecvOnly:846 s = "<-chan "847 case types.SendRecv:848 s = "chan "849 default:850 panic("unreachable")851 }852 buf.WriteString(s)853 w.writeType(buf, typ.Elem())854855 case *types.Alias:856 w.writeType(buf, types.Unalias(typ))857858 case *types.Named:859 obj := typ.Obj()860 pkg := obj.Pkg()861 if pkg != nil && pkg != w.current.Package {862 buf.WriteString(pkg.Name())863 buf.WriteByte('.')864 }865 buf.WriteString(typ.Obj().Name())866 if targs := typ.TypeArgs(); targs.Len() > 0 {867 buf.WriteByte('[')868 for i := 0; i < targs.Len(); i++ {869 if i > 0 {870 buf.WriteString(", ")871 }872 w.writeType(buf, targs.At(i))873 }874 buf.WriteByte(']')875 }876877 case *types.TypeParam:878 // Type parameter names may change, so use a placeholder instead.879 fmt.Fprintf(buf, "$%d", typ.Index())880881 default:882 panic(fmt.Sprintf("unknown type %T", typ))883 }884}885886func (w *Walker) writeSignature(buf *bytes.Buffer, sig *types.Signature) {887 if tparams := sig.TypeParams(); tparams != nil {888 w.writeTypeParams(buf, tparams, true)889 }890 w.writeParams(buf, sig.Params(), sig.Variadic())891 switch res := sig.Results(); res.Len() {892 case 0:893 // nothing to do894 case 1:895 buf.WriteByte(' ')896 w.writeType(buf, res.At(0).Type())897 default:898 buf.WriteByte(' ')899 w.writeParams(buf, res, false)900 }901}902903func (w *Walker) writeTypeParams(buf *bytes.Buffer, tparams *types.TypeParamList, withConstraints bool) {904 buf.WriteByte('[')905 c := tparams.Len()906 for i := 0; i < c; i++ {907 if i > 0 {908 buf.WriteString(", ")909 }910 tp := tparams.At(i)911 w.writeType(buf, tp)912 if withConstraints {913 buf.WriteByte(' ')914 w.writeType(buf, tp.Constraint())915 }916 }917 buf.WriteByte(']')918}919920func (w *Walker) writeParams(buf *bytes.Buffer, t *types.Tuple, variadic bool) {921 buf.WriteByte('(')922 for i, n := 0, t.Len(); i < n; i++ {923 if i > 0 {924 buf.WriteString(", ")925 }926 typ := t.At(i).Type()927 if variadic && i+1 == n {928 buf.WriteString("...")929 typ = typ.(*types.Slice).Elem()930 }931 w.writeType(buf, typ)932 }933 buf.WriteByte(')')934}935936func (w *Walker) typeString(typ types.Type) string {937 var buf bytes.Buffer938 w.writeType(&buf, typ)939 return buf.String()940}941942func (w *Walker) signatureString(sig *types.Signature) string {943 var buf bytes.Buffer944 w.writeSignature(&buf, sig)945 return buf.String()946}947948func (w *Walker) emitObj(obj types.Object) {949 switch obj := obj.(type) {950 case *types.Const:951 if w.isDeprecated(obj) {952 w.emitf("const %s //deprecated", obj.Name())953 }954 w.emitf("const %s %s", obj.Name(), w.typeString(obj.Type()))955 x := obj.Val()956 short := x.String()957 exact := x.ExactString()958 if short == exact {959 w.emitf("const %s = %s", obj.Name(), short)960 } else {961 w.emitf("const %s = %s // %s", obj.Name(), short, exact)962 }963 case *types.Var:964 if w.isDeprecated(obj) {965 w.emitf("var %s //deprecated", obj.Name())966 }967 w.emitf("var %s %s", obj.Name(), w.typeString(obj.Type()))968 case *types.TypeName:969 w.emitType(obj)970 case *types.Func:971 w.emitFunc(obj)972 default:973 panic("unknown object: " + obj.String())974 }975}976977func (w *Walker) emitType(obj *types.TypeName) {978 name := obj.Name()979 if w.isDeprecated(obj) {980 w.emitf("type %s //deprecated", name)981 }982 typ := obj.Type()983 if obj.IsAlias() {984 w.emitf("type %s = %s", name, w.typeString(typ))985 return986 }987 if tparams := obj.Type().(*types.Named).TypeParams(); tparams != nil {988 var buf bytes.Buffer989 buf.WriteString(name)990 w.writeTypeParams(&buf, tparams, true)991 name = buf.String()992 }993 switch typ := typ.Underlying().(type) {994 case *types.Struct:995 w.emitStructType(name, typ)996 case *types.Interface:997 w.emitIfaceType(name, typ)998 return // methods are handled by emitIfaceType999 default:1000 w.emitf("type %s %s", name, w.typeString(typ.Underlying()))1001 }10021003 // emit methods with value receiver1004 var methodNames map[string]bool1005 vset := types.NewMethodSet(typ)1006 for i, n := 0, vset.Len(); i < n; i++ {1007 m := vset.At(i)1008 if m.Obj().Exported() {1009 w.emitMethod(m)1010 if methodNames == nil {1011 methodNames = make(map[string]bool)1012 }1013 methodNames[m.Obj().Name()] = true1014 }1015 }10161017 // emit methods with pointer receiver; exclude1018 // methods that we have emitted already1019 // (the method set of *T includes the methods of T)1020 pset := types.NewMethodSet(types.NewPointer(typ))1021 for i, n := 0, pset.Len(); i < n; i++ {1022 m := pset.At(i)1023 if m.Obj().Exported() && !methodNames[m.Obj().Name()] {1024 w.emitMethod(m)1025 }1026 }1027}10281029func (w *Walker) emitStructType(name string, typ *types.Struct) {1030 typeStruct := fmt.Sprintf("type %s struct", name)1031 w.emitf("%s", typeStruct)1032 defer w.pushScope(typeStruct)()10331034 for i := 0; i < typ.NumFields(); i++ {1035 f := typ.Field(i)1036 if !f.Exported() {1037 continue1038 }1039 typ := f.Type()1040 if f.Anonymous() {1041 if w.isDeprecated(f) {1042 w.emitf("embedded %s //deprecated", w.typeString(typ))1043 }1044 w.emitf("embedded %s", w.typeString(typ))1045 continue1046 }1047 if w.isDeprecated(f) {1048 w.emitf("%s //deprecated", f.Name())1049 }1050 w.emitf("%s %s", f.Name(), w.typeString(typ))1051 }1052}10531054func (w *Walker) emitIfaceType(name string, typ *types.Interface) {1055 pop := w.pushScope("type " + name + " interface")10561057 var methodNames []string1058 complete := true1059 mset := types.NewMethodSet(typ)1060 for i, n := 0, mset.Len(); i < n; i++ {1061 m := mset.At(i).Obj().(*types.Func)1062 if !m.Exported() {1063 complete = false1064 continue1065 }1066 methodNames = append(methodNames, m.Name())1067 if w.isDeprecated(m) {1068 w.emitf("%s //deprecated", m.Name())1069 }1070 w.emitf("%s%s", m.Name(), w.signatureString(m.Signature()))1071 }10721073 if !complete {1074 // The method set has unexported methods, so all the1075 // implementations are provided by the same package,1076 // so the method set can be extended. Instead of recording1077 // the full set of names (below), record only that there were1078 // unexported methods. (If the interface shrinks, we will notice1079 // because a method signature emitted during the last loop1080 // will disappear.)1081 w.emitf("unexported methods")1082 }10831084 pop()10851086 if !complete {1087 return1088 }10891090 if len(methodNames) == 0 {1091 w.emitf("type %s interface {}", name)1092 return1093 }10941095 slices.Sort(methodNames)1096 w.emitf("type %s interface { %s }", name, strings.Join(methodNames, ", "))1097}10981099func (w *Walker) emitFunc(f *types.Func) {1100 sig := f.Signature()1101 if sig.Recv() != nil {1102 panic("method considered a regular function: " + f.String())1103 }1104 if w.isDeprecated(f) {1105 w.emitf("func %s //deprecated", f.Name())1106 }1107 w.emitf("func %s%s", f.Name(), w.signatureString(sig))1108}11091110func (w *Walker) emitMethod(m *types.Selection) {1111 sig := m.Type().(*types.Signature)1112 recv := sig.Recv().Type()1113 // report exported methods with unexported receiver base type1114 if true {1115 base := recv1116 if p, _ := recv.(*types.Pointer); p != nil {1117 base = p.Elem()1118 }1119 if obj := base.(*types.Named).Obj(); !obj.Exported() {1120 log.Fatalf("exported method with unexported receiver base type: %s", m)1121 }1122 }1123 tps := ""1124 if rtp := sig.RecvTypeParams(); rtp != nil {1125 var buf bytes.Buffer1126 w.writeTypeParams(&buf, rtp, false)1127 tps = buf.String()1128 }1129 if w.isDeprecated(m.Obj()) {1130 w.emitf("method (%s%s) %s //deprecated", w.typeString(recv), tps, m.Obj().Name())1131 }1132 w.emitf("method (%s%s) %s%s", w.typeString(recv), tps, m.Obj().Name(), w.signatureString(sig))1133}11341135func (w *Walker) emitf(format string, args ...any) {1136 f := strings.Join(w.scope, ", ") + ", " + fmt.Sprintf(format, args...)1137 if strings.Contains(f, "\n") {1138 panic("feature contains newlines: " + f)1139 }11401141 if _, dup := w.features[f]; dup {1142 panic("duplicate feature inserted: " + f)1143 }1144 w.features[f] = true11451146 if verbose {1147 log.Printf("feature: %s", f)1148 }1149}11501151func needApproval(filename string) bool {1152 name := filepath.Base(filename)1153 if name == "go1.txt" {1154 return false1155 }1156 minor := strings.TrimSuffix(strings.TrimPrefix(name, "go1."), ".txt")1157 n, err := strconv.Atoi(minor)1158 if err != nil {1159 log.Fatalf("unexpected api file: %v", name)1160 }1161 return n >= 19 // started tracking approvals in Go 1.191162}11631164func (w *Walker) collectDeprecated() {1165 isDeprecated := func(doc *ast.CommentGroup) bool {1166 // Look for "Deprecated:" (case-sensitive) at the beginning (not middle) of a paragraph.1167 // It's typically found in the last paragraph, but it's not required to be the last one.1168 // The colon is typically followed by a space, but it can also be a newline, as was the1169 // case at https://go.dev/pkg/go/build#AllowBinary for example.1170 //1171 // See https://go.dev/wiki/Deprecated and https://go.dev/ref/mod#go-mod-file-module-deprecation.1172 text := doc.Text()1173 return strings.HasPrefix(text, "Deprecated: ") || strings.Contains(text, "\n\nDeprecated: ") ||1174 strings.HasPrefix(text, "Deprecated:\n") || strings.Contains(text, "\n\nDeprecated:\n")1175 }11761177 w.deprecated = make(map[token.Pos]bool)1178 mark := func(id *ast.Ident) {1179 if id != nil {1180 w.deprecated[id.Pos()] = true1181 }1182 }1183 for _, file := range w.current.Files {1184 ast.Inspect(file, func(n ast.Node) bool {1185 switch n := n.(type) {1186 case *ast.File:1187 if isDeprecated(n.Doc) {1188 mark(n.Name)1189 }1190 return true1191 case *ast.GenDecl:1192 if isDeprecated(n.Doc) {1193 for _, spec := range n.Specs {1194 switch spec := spec.(type) {1195 case *ast.ValueSpec:1196 for _, id := range spec.Names {1197 mark(id)1198 }1199 case *ast.TypeSpec:1200 mark(spec.Name)1201 }1202 }1203 }1204 return true // look at specs1205 case *ast.FuncDecl:1206 if isDeprecated(n.Doc) {1207 mark(n.Name)1208 }1209 return false1210 case *ast.TypeSpec:1211 if isDeprecated(n.Doc) {1212 mark(n.Name)1213 }1214 return true // recurse into struct or interface type1215 case *ast.StructType:1216 return true // recurse into fields1217 case *ast.InterfaceType:1218 return true // recurse into methods1219 case *ast.FieldList:1220 return true // recurse into fields1221 case *ast.ValueSpec:1222 if isDeprecated(n.Doc) {1223 for _, id := range n.Names {1224 mark(id)1225 }1226 }1227 return false1228 case *ast.Field:1229 if isDeprecated(n.Doc) {1230 for _, id := range n.Names {1231 mark(id)1232 }1233 if len(n.Names) == 0 {1234 // embedded field T or *T?1235 typ := n.Type1236 if ptr, ok := typ.(*ast.StarExpr); ok {1237 typ = ptr.X1238 }1239 if id, ok := typ.(*ast.Ident); ok {1240 mark(id)1241 }1242 }1243 }1244 return false1245 default:1246 return false1247 }1248 })1249 }1250}12511252func (w *Walker) isDeprecated(obj types.Object) bool {1253 return w.deprecated[obj.Pos()]1254}
Findings
✓ No findings reported for this file.