stdlib_test.go raw

   1  // Copyright 2013 The Go Authors. All rights reserved.
   2  // Use of this source code is governed by a BSD-style
   3  // license that can be found in the LICENSE file.
   4  
   5  // This file tests types.Check by using it to
   6  // typecheck the standard library and tests.
   7  
   8  package types_test
   9  
  10  import (
  11  	"errors"
  12  	"fmt"
  13  	"go/ast"
  14  	"go/build"
  15  	"go/importer"
  16  	"go/parser"
  17  	"go/scanner"
  18  	"go/token"
  19  	"internal/testenv"
  20  	"os"
  21  	"path/filepath"
  22  	"runtime"
  23  	"slices"
  24  	"strings"
  25  	"sync"
  26  	"testing"
  27  	"time"
  28  
  29  	. "go/types"
  30  )
  31  
  32  // The cmd/*/internal packages may have been deleted as part of a binary
  33  // release. Import from source instead.
  34  //
  35  // (See https://golang.org/issue/43232 and
  36  // https://github.com/golang/build/blob/df58bbac082bc87c4a3cdfe336d1ffe60bbaa916/cmd/release/release.go#L533-L545.)
  37  //
  38  // Use the same importer for all std lib tests to
  39  // avoid repeated importing of the same packages.
  40  var stdLibImporter = importer.ForCompiler(token.NewFileSet(), "source", nil)
  41  
  42  func TestStdlib(t *testing.T) {
  43  	if testing.Short() {
  44  		t.Skip("skipping in short mode")
  45  	}
  46  
  47  	testenv.MustHaveGoBuild(t)
  48  
  49  	// Collect non-test files.
  50  	dirFiles := make(map[string][]string)
  51  	root := filepath.Join(testenv.GOROOT(t), "src")
  52  	walkPkgDirs(root, func(dir string, filenames []string) {
  53  		dirFiles[dir] = filenames
  54  	}, t.Error)
  55  
  56  	c := &stdlibChecker{
  57  		dirFiles: dirFiles,
  58  		pkgs:     make(map[string]*futurePackage),
  59  	}
  60  
  61  	start := time.Now()
  62  
  63  	// Though we read files while parsing, type-checking is otherwise CPU bound.
  64  	//
  65  	// This doesn't achieve great CPU utilization as many packages may block
  66  	// waiting for a common import, but in combination with the non-deterministic
  67  	// map iteration below this should provide decent coverage of concurrent
  68  	// type-checking (see golang/go#47729).
  69  	cpulimit := make(chan struct{}, runtime.GOMAXPROCS(0))
  70  	var wg sync.WaitGroup
  71  
  72  	for dir := range dirFiles {
  73  		dir := dir
  74  
  75  		cpulimit <- struct{}{}
  76  		wg.Add(1)
  77  		go func() {
  78  			defer func() {
  79  				wg.Done()
  80  				<-cpulimit
  81  			}()
  82  
  83  			_, err := c.getDirPackage(dir)
  84  			if err != nil {
  85  				t.Errorf("error checking %s: %v", dir, err)
  86  			}
  87  		}()
  88  	}
  89  
  90  	wg.Wait()
  91  
  92  	if testing.Verbose() {
  93  		fmt.Println(len(dirFiles), "packages typechecked in", time.Since(start))
  94  	}
  95  }
  96  
  97  // stdlibChecker implements concurrent type-checking of the packages defined by
  98  // dirFiles, which must define a closed set of packages (such as GOROOT/src).
  99  type stdlibChecker struct {
 100  	dirFiles map[string][]string // non-test files per directory; must be pre-populated
 101  
 102  	mu   sync.Mutex
 103  	pkgs map[string]*futurePackage // future cache of type-checking results
 104  }
 105  
 106  // A futurePackage is a future result of type-checking.
 107  type futurePackage struct {
 108  	done chan struct{} // guards pkg and err
 109  	pkg  *Package
 110  	err  error
 111  }
 112  
 113  func (c *stdlibChecker) Import(path string) (*Package, error) {
 114  	panic("unimplemented: use ImportFrom")
 115  }
 116  
 117  func (c *stdlibChecker) ImportFrom(path, dir string, _ ImportMode) (*Package, error) {
 118  	if path == "unsafe" {
 119  		// unsafe cannot be type checked normally.
 120  		return Unsafe, nil
 121  	}
 122  
 123  	p, err := build.Default.Import(path, dir, build.FindOnly)
 124  	if err != nil {
 125  		return nil, err
 126  	}
 127  
 128  	pkg, err := c.getDirPackage(p.Dir)
 129  	if pkg != nil {
 130  		// As long as pkg is non-nil, avoid redundant errors related to failed
 131  		// imports. TestStdlib will collect errors once for each package.
 132  		return pkg, nil
 133  	}
 134  	return nil, err
 135  }
 136  
 137  // getDirPackage gets the package defined in dir from the future cache.
 138  //
 139  // If this is the first goroutine requesting the package, getDirPackage
 140  // type-checks.
 141  func (c *stdlibChecker) getDirPackage(dir string) (*Package, error) {
 142  	c.mu.Lock()
 143  	fut, ok := c.pkgs[dir]
 144  	if !ok {
 145  		// First request for this package dir; type check.
 146  		fut = &futurePackage{
 147  			done: make(chan struct{}),
 148  		}
 149  		c.pkgs[dir] = fut
 150  		files, ok := c.dirFiles[dir]
 151  		c.mu.Unlock()
 152  		if !ok {
 153  			fut.err = fmt.Errorf("no files for %s", dir)
 154  		} else {
 155  			// Using dir as the package path here may be inconsistent with the behavior
 156  			// of a normal importer, but is sufficient as dir is by construction unique
 157  			// to this package.
 158  			fut.pkg, fut.err = typecheckFiles(dir, files, c)
 159  		}
 160  		close(fut.done)
 161  	} else {
 162  		// Otherwise, await the result.
 163  		c.mu.Unlock()
 164  		<-fut.done
 165  	}
 166  	return fut.pkg, fut.err
 167  }
 168  
 169  // firstComment returns the contents of the first non-empty comment in
 170  // the given file, "skip", or the empty string. No matter the present
 171  // comments, if any of them contains a build tag, the result is always
 172  // "skip". Only comments before the "package" token and within the first
 173  // 4K of the file are considered.
 174  func firstComment(filename string) string {
 175  	f, err := os.Open(filename)
 176  	if err != nil {
 177  		return ""
 178  	}
 179  	defer f.Close()
 180  
 181  	var src [4 << 10]byte // read at most 4KB
 182  	n, _ := f.Read(src[:])
 183  
 184  	var first string
 185  	var s scanner.Scanner
 186  	s.Init(fset.AddFile("", fset.Base(), n), src[:n], nil /* ignore errors */, scanner.ScanComments)
 187  	for {
 188  		_, tok, lit := s.Scan()
 189  		switch tok {
 190  		case token.COMMENT:
 191  			// remove trailing */ of multi-line comment
 192  			if lit[1] == '*' {
 193  				lit = lit[:len(lit)-2]
 194  			}
 195  			contents := strings.TrimSpace(lit[2:])
 196  			if strings.HasPrefix(contents, "go:build ") {
 197  				return "skip"
 198  			}
 199  			if first == "" {
 200  				first = contents // contents may be "" but that's ok
 201  			}
 202  			// continue as we may still see build tags
 203  
 204  		case token.PACKAGE, token.EOF:
 205  			return first
 206  		}
 207  	}
 208  }
 209  
 210  func testTestDir(t *testing.T, path string, ignore ...string) {
 211  	files, err := os.ReadDir(path)
 212  	if err != nil {
 213  		// cmd/distpack deletes GOROOT/test, so skip the test if it isn't present.
 214  		// cmd/distpack also requires GOROOT/VERSION to exist, so use that to
 215  		// suppress false-positive skips.
 216  		if _, err := os.Stat(filepath.Join(testenv.GOROOT(t), "test")); os.IsNotExist(err) {
 217  			if _, err := os.Stat(filepath.Join(testenv.GOROOT(t), "VERSION")); err == nil {
 218  				t.Skipf("skipping: GOROOT/test not present")
 219  			}
 220  		}
 221  		t.Fatal(err)
 222  	}
 223  
 224  	excluded := make(map[string]bool)
 225  	for _, filename := range ignore {
 226  		excluded[filename] = true
 227  	}
 228  
 229  	fset := token.NewFileSet()
 230  	for _, f := range files {
 231  		// filter directory contents
 232  		if f.IsDir() || !strings.HasSuffix(f.Name(), ".go") || excluded[f.Name()] {
 233  			continue
 234  		}
 235  
 236  		// get per-file instructions
 237  		expectErrors := false
 238  		filename := filepath.Join(path, f.Name())
 239  		goVersion := ""
 240  		if comment := firstComment(filename); comment != "" {
 241  			if strings.Contains(comment, "-goexperiment") {
 242  				continue // ignore this file
 243  			}
 244  			fields := strings.Fields(comment)
 245  			switch fields[0] {
 246  			case "skip", "compiledir":
 247  				continue // ignore this file
 248  			case "errorcheck":
 249  				expectErrors = true
 250  				for _, arg := range fields[1:] {
 251  					if arg == "-0" || arg == "-+" || arg == "-std" {
 252  						// Marked explicitly as not expecting errors (-0),
 253  						// or marked as compiling runtime/stdlib, which is only done
 254  						// to trigger runtime/stdlib-only error output.
 255  						// In both cases, the code should typecheck.
 256  						expectErrors = false
 257  						break
 258  					}
 259  					const prefix = "-lang="
 260  					if strings.HasPrefix(arg, prefix) {
 261  						goVersion = arg[len(prefix):]
 262  					}
 263  				}
 264  			}
 265  		}
 266  
 267  		// parse and type-check file
 268  		file, err := parser.ParseFile(fset, filename, nil, 0)
 269  		if err == nil {
 270  			conf := Config{
 271  				GoVersion: goVersion,
 272  				Importer:  stdLibImporter,
 273  			}
 274  			_, err = conf.Check(filename, fset, []*ast.File{file}, nil)
 275  		}
 276  
 277  		if expectErrors {
 278  			if err == nil {
 279  				t.Errorf("expected errors but found none in %s", filename)
 280  			}
 281  		} else {
 282  			if err != nil {
 283  				t.Error(err)
 284  			}
 285  		}
 286  	}
 287  }
 288  
 289  func TestStdTest(t *testing.T) {
 290  	testenv.MustHaveGoBuild(t)
 291  
 292  	if testing.Short() && testenv.Builder() == "" {
 293  		t.Skip("skipping in short mode")
 294  	}
 295  
 296  	testTestDir(t, filepath.Join(testenv.GOROOT(t), "test"),
 297  		"cmplxdivide.go", // also needs file cmplxdivide1.go - ignore
 298  		"directive.go",   // tests compiler rejection of bad directive placement - ignore
 299  		"directive2.go",  // tests compiler rejection of bad directive placement - ignore
 300  		"embedfunc.go",   // tests //go:embed
 301  		"embedvers.go",   // tests //go:embed
 302  		"linkname2.go",   // go/types doesn't check validity of //go:xxx directives
 303  		"linkname3.go",   // go/types doesn't check validity of //go:xxx directives
 304  	)
 305  }
 306  
 307  func TestStdFixed(t *testing.T) {
 308  	testenv.MustHaveGoBuild(t)
 309  
 310  	if testing.Short() && testenv.Builder() == "" {
 311  		t.Skip("skipping in short mode")
 312  	}
 313  
 314  	testTestDir(t, filepath.Join(testenv.GOROOT(t), "test", "fixedbugs"),
 315  		"bug248.go", "bug302.go", "bug369.go", // complex test instructions - ignore
 316  		"bug398.go",      // go/types doesn't check for anonymous interface cycles (go.dev/issue/56103)
 317  		"issue6889.go",   // gc-specific test
 318  		"issue11362.go",  // canonical import path check
 319  		"issue16369.go",  // go/types handles this correctly - not an issue
 320  		"issue18459.go",  // go/types doesn't check validity of //go:xxx directives
 321  		"issue18882.go",  // go/types doesn't check validity of //go:xxx directives
 322  		"issue20027.go",  // go/types does not have constraints on channel element size
 323  		"issue20529.go",  // go/types does not have constraints on stack size
 324  		"issue22200.go",  // go/types does not have constraints on stack size
 325  		"issue22200b.go", // go/types does not have constraints on stack size
 326  		"issue25507.go",  // go/types does not have constraints on stack size
 327  		"issue20780.go",  // go/types does not have constraints on stack size
 328  		"bug251.go",      // go.dev/issue/34333 which was exposed with fix for go.dev/issue/34151
 329  		"issue42058a.go", // go/types does not have constraints on channel element size
 330  		"issue42058b.go", // go/types does not have constraints on channel element size
 331  		"issue48097.go",  // go/types doesn't check validity of //go:xxx directives, and non-init bodyless function
 332  		"issue48230.go",  // go/types doesn't check validity of //go:xxx directives
 333  		"issue49767.go",  // go/types does not have constraints on channel element size
 334  		"issue49814.go",  // go/types does not have constraints on array size
 335  		"issue56103.go",  // anonymous interface cycles; will be a type checker error in 1.22
 336  		"issue52697.go",  // go/types does not have constraints on stack size
 337  		"issue73309.go",  // this test requires GODEBUG=gotypesalias=1
 338  		"issue73309b.go", // this test requires GODEBUG=gotypesalias=1
 339  
 340  		// These tests requires runtime/cgo.Incomplete, which is only available on some platforms.
 341  		// However, go/types does not know about build constraints.
 342  		"bug514.go",
 343  		"issue40954.go",
 344  		"issue42032.go",
 345  		"issue42076.go",
 346  		"issue46903.go",
 347  		"issue51733.go",
 348  		"notinheap2.go",
 349  		"notinheap3.go",
 350  	)
 351  }
 352  
 353  func TestStdKen(t *testing.T) {
 354  	testenv.MustHaveGoBuild(t)
 355  
 356  	testTestDir(t, filepath.Join(testenv.GOROOT(t), "test", "ken"))
 357  }
 358  
 359  // Package paths of excluded packages.
 360  var excluded = map[string]bool{
 361  	"builtin":                       true,
 362  	"cmd/compile/internal/ssa/_gen": true,
 363  }
 364  
 365  // printPackageMu synchronizes the printing of type-checked package files in
 366  // the typecheckFiles function.
 367  //
 368  // Without synchronization, package files may be interleaved during concurrent
 369  // type-checking.
 370  var printPackageMu sync.Mutex
 371  
 372  // typecheckFiles typechecks the given package files.
 373  func typecheckFiles(path string, filenames []string, importer Importer) (*Package, error) {
 374  	fset := token.NewFileSet()
 375  
 376  	// Parse package files.
 377  	var files []*ast.File
 378  	for _, filename := range filenames {
 379  		file, err := parser.ParseFile(fset, filename, nil, parser.AllErrors)
 380  		if err != nil {
 381  			return nil, err
 382  		}
 383  
 384  		files = append(files, file)
 385  	}
 386  
 387  	if testing.Verbose() {
 388  		printPackageMu.Lock()
 389  		fmt.Println("package", files[0].Name.Name)
 390  		for _, filename := range filenames {
 391  			fmt.Println("\t", filename)
 392  		}
 393  		printPackageMu.Unlock()
 394  	}
 395  
 396  	// Typecheck package files.
 397  	var errs []error
 398  	conf := Config{
 399  		Error: func(err error) {
 400  			errs = append(errs, err)
 401  		},
 402  		Importer: importer,
 403  	}
 404  	info := Info{Uses: make(map[*ast.Ident]Object)}
 405  	pkg, _ := conf.Check(path, fset, files, &info)
 406  	err := errors.Join(errs...)
 407  	if err != nil {
 408  		return pkg, err
 409  	}
 410  
 411  	// Perform checks of API invariants.
 412  
 413  	// All Objects have a package, except predeclared ones.
 414  	errorError := Universe.Lookup("error").Type().Underlying().(*Interface).ExplicitMethod(0) // (error).Error
 415  	for id, obj := range info.Uses {
 416  		predeclared := obj == Universe.Lookup(obj.Name()) || obj == errorError
 417  		if predeclared == (obj.Pkg() != nil) {
 418  			posn := fset.Position(id.Pos())
 419  			if predeclared {
 420  				return nil, fmt.Errorf("%s: predeclared object with package: %s", posn, obj)
 421  			} else {
 422  				return nil, fmt.Errorf("%s: user-defined object without package: %s", posn, obj)
 423  			}
 424  		}
 425  	}
 426  
 427  	return pkg, nil
 428  }
 429  
 430  // pkgFilenames returns the list of package filenames for the given directory.
 431  func pkgFilenames(dir string, includeTest bool) ([]string, error) {
 432  	ctxt := build.Default
 433  	ctxt.CgoEnabled = false
 434  	pkg, err := ctxt.ImportDir(dir, 0)
 435  	if err != nil {
 436  		if _, nogo := err.(*build.NoGoError); nogo {
 437  			return nil, nil // no *.go files, not an error
 438  		}
 439  		return nil, err
 440  	}
 441  	if excluded[pkg.ImportPath] {
 442  		return nil, nil
 443  	}
 444  	if slices.Contains(strings.Split(pkg.ImportPath, "/"), "_asm") {
 445  		// Submodules where not all dependencies are available.
 446  		// See go.dev/issue/46027.
 447  		return nil, nil
 448  	}
 449  	var filenames []string
 450  	for _, name := range pkg.GoFiles {
 451  		filenames = append(filenames, filepath.Join(pkg.Dir, name))
 452  	}
 453  	if includeTest {
 454  		for _, name := range pkg.TestGoFiles {
 455  			filenames = append(filenames, filepath.Join(pkg.Dir, name))
 456  		}
 457  	}
 458  	return filenames, nil
 459  }
 460  
 461  func walkPkgDirs(dir string, pkgh func(dir string, filenames []string), errh func(args ...any)) {
 462  	w := walker{pkgh, errh}
 463  	w.walk(dir)
 464  }
 465  
 466  type walker struct {
 467  	pkgh func(dir string, filenames []string)
 468  	errh func(args ...any)
 469  }
 470  
 471  func (w *walker) walk(dir string) {
 472  	files, err := os.ReadDir(dir)
 473  	if err != nil {
 474  		w.errh(err)
 475  		return
 476  	}
 477  
 478  	// apply pkgh to the files in directory dir
 479  
 480  	// Don't get test files as these packages are imported.
 481  	pkgFiles, err := pkgFilenames(dir, false)
 482  	if err != nil {
 483  		w.errh(err)
 484  		return
 485  	}
 486  	if pkgFiles != nil {
 487  		w.pkgh(dir, pkgFiles)
 488  	}
 489  
 490  	// traverse subdirectories, but don't walk into testdata
 491  	for _, f := range files {
 492  		if f.IsDir() && f.Name() != "testdata" {
 493  			w.walk(filepath.Join(dir, f.Name()))
 494  		}
 495  	}
 496  }
 497