src/cmd/api/main_test.go GO 1,255 lines View on github.com → Search inside
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.

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.