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