1package build
2
3import (
4	"fmt"
5	gobuild "go/build"
6	"go/token"
7	"strconv"
8	"strings"
9	"testing"
10
11	"github.com/kisielk/gotool"
12	"github.com/shurcooL/go/importgraphutil"
13)
14
15// Natives augment the standard library with GopherJS-specific changes.
16// This test ensures that none of the standard library packages are modified
17// in a way that adds imports which the original upstream standard library package
18// does not already import. Doing that can increase generated output size or cause
19// other unexpected issues (since the cmd/go tool does not know about these extra imports),
20// so it's best to avoid it.
21//
22// It checks all standard library packages. Each package is considered as a normal
23// package, as a test package, and as an external test package.
24func TestNativesDontImportExtraPackages(t *testing.T) {
25	// Calculate the forward import graph for all standard library packages.
26	// It's needed for populateImportSet.
27	stdOnly := gobuild.Default
28	stdOnly.GOPATH = "" // We only care about standard library, so skip all GOPATH packages.
29	forward, _, err := importgraphutil.BuildNoTests(&stdOnly)
30	if err != nil {
31		t.Fatalf("importgraphutil.BuildNoTests: %v", err)
32	}
33
34	// populateImportSet takes a slice of imports, and populates set with those
35	// imports, as well as their transitive dependencies. That way, the set can
36	// be quickly queried to check if a package is in the import graph of imports.
37	//
38	// Note, this does not include transitive imports of test/xtest packages,
39	// which could cause some false positives. It currently doesn't, but if it does,
40	// then support for that should be added here.
41	populateImportSet := func(imports []string, set *stringSet) {
42		for _, p := range imports {
43			(*set)[p] = struct{}{}
44			switch p {
45			case "sync":
46				(*set)["github.com/gopherjs/gopherjs/nosync"] = struct{}{}
47			}
48			transitiveImports := forward.Search(p)
49			for p := range transitiveImports {
50				(*set)[p] = struct{}{}
51			}
52		}
53	}
54
55	// Check all standard library packages.
56	//
57	// The general strategy is to first import each standard library package using the
58	// normal build.Import, which returns a *build.Package. That contains Imports, TestImports,
59	// and XTestImports values that are considered the "real imports".
60	//
61	// That list of direct imports is then expanded to the transitive closure by populateImportSet,
62	// meaning all packages that are indirectly imported are also added to the set.
63	//
64	// Then, github.com/gopherjs/gopherjs/build.parseAndAugment(*build.Package) returns []*ast.File.
65	// Those augmented parsed Go files of the package are checked, one file at at time, one import
66	// at a time. Each import is verified to belong in the set of allowed real imports.
67	for _, pkg := range gotool.ImportPaths([]string{"std"}) {
68		// Normal package.
69		{
70			// Import the real normal package, and populate its real import set.
71			bpkg, err := gobuild.Import(pkg, "", gobuild.ImportComment)
72			if err != nil {
73				t.Fatalf("gobuild.Import: %v", err)
74			}
75			realImports := make(stringSet)
76			populateImportSet(bpkg.Imports, &realImports)
77
78			// Use parseAndAugment to get a list of augmented AST files.
79			fset := token.NewFileSet()
80			files, err := parseAndAugment(NewBuildContext("", nil), bpkg, false, fset)
81			if err != nil {
82				t.Fatalf("github.com/gopherjs/gopherjs/build.parseAndAugment: %v", err)
83			}
84
85			// Verify imports of normal augmented AST files.
86			for _, f := range files {
87				fileName := fset.File(f.Pos()).Name()
88				normalFile := !strings.HasSuffix(fileName, "_test.go")
89				if !normalFile {
90					continue
91				}
92				for _, imp := range f.Imports {
93					importPath, err := strconv.Unquote(imp.Path.Value)
94					if err != nil {
95						t.Fatalf("strconv.Unquote(%v): %v", imp.Path.Value, err)
96					}
97					if importPath == "github.com/gopherjs/gopherjs/js" {
98						continue
99					}
100					if _, ok := realImports[importPath]; !ok {
101						t.Errorf("augmented normal package %q imports %q in file %v, but real %q doesn't:\nrealImports = %v", bpkg.ImportPath, importPath, fileName, bpkg.ImportPath, realImports)
102					}
103				}
104			}
105		}
106
107		// Test package.
108		{
109			// Import the real test package, and populate its real import set.
110			bpkg, err := gobuild.Import(pkg, "", gobuild.ImportComment)
111			if err != nil {
112				t.Fatalf("gobuild.Import: %v", err)
113			}
114			realTestImports := make(stringSet)
115			populateImportSet(bpkg.TestImports, &realTestImports)
116
117			// Use parseAndAugment to get a list of augmented AST files.
118			fset := token.NewFileSet()
119			files, err := parseAndAugment(NewBuildContext("", nil), bpkg, true, fset)
120			if err != nil {
121				t.Fatalf("github.com/gopherjs/gopherjs/build.parseAndAugment: %v", err)
122			}
123
124			// Verify imports of test augmented AST files.
125			for _, f := range files {
126				fileName, pkgName := fset.File(f.Pos()).Name(), f.Name.String()
127				testFile := strings.HasSuffix(fileName, "_test.go") && !strings.HasSuffix(pkgName, "_test")
128				if !testFile {
129					continue
130				}
131				for _, imp := range f.Imports {
132					importPath, err := strconv.Unquote(imp.Path.Value)
133					if err != nil {
134						t.Fatalf("strconv.Unquote(%v): %v", imp.Path.Value, err)
135					}
136					if importPath == "github.com/gopherjs/gopherjs/js" {
137						continue
138					}
139					if _, ok := realTestImports[importPath]; !ok {
140						t.Errorf("augmented test package %q imports %q in file %v, but real %q doesn't:\nrealTestImports = %v", bpkg.ImportPath, importPath, fileName, bpkg.ImportPath, realTestImports)
141					}
142				}
143			}
144		}
145
146		// External test package.
147		{
148			// Import the real external test package, and populate its real import set.
149			bpkg, err := gobuild.Import(pkg, "", gobuild.ImportComment)
150			if err != nil {
151				t.Fatalf("gobuild.Import: %v", err)
152			}
153			realXTestImports := make(stringSet)
154			populateImportSet(bpkg.XTestImports, &realXTestImports)
155
156			// Add _test suffix to import path to cause parseAndAugment to use external test mode.
157			bpkg.ImportPath += "_test"
158
159			// Use parseAndAugment to get a list of augmented AST files, then check only the external test files.
160			fset := token.NewFileSet()
161			files, err := parseAndAugment(NewBuildContext("", nil), bpkg, true, fset)
162			if err != nil {
163				t.Fatalf("github.com/gopherjs/gopherjs/build.parseAndAugment: %v", err)
164			}
165
166			// Verify imports of external test augmented AST files.
167			for _, f := range files {
168				fileName, pkgName := fset.File(f.Pos()).Name(), f.Name.String()
169				xTestFile := strings.HasSuffix(fileName, "_test.go") && strings.HasSuffix(pkgName, "_test")
170				if !xTestFile {
171					continue
172				}
173				for _, imp := range f.Imports {
174					importPath, err := strconv.Unquote(imp.Path.Value)
175					if err != nil {
176						t.Fatalf("strconv.Unquote(%v): %v", imp.Path.Value, err)
177					}
178					if importPath == "github.com/gopherjs/gopherjs/js" {
179						continue
180					}
181					if _, ok := realXTestImports[importPath]; !ok {
182						t.Errorf("augmented external test package %q imports %q in file %v, but real %q doesn't:\nrealXTestImports = %v", bpkg.ImportPath, importPath, fileName, bpkg.ImportPath, realXTestImports)
183					}
184				}
185			}
186		}
187	}
188}
189
190// stringSet is used to print a set of strings in a more readable way.
191type stringSet map[string]struct{}
192
193func (m stringSet) String() string {
194	s := make([]string, 0, len(m))
195	for v := range m {
196		s = append(s, v)
197	}
198	return fmt.Sprintf("%q", s)
199}
200