From f565ca5a233a55b710ee93b130f31ec95c938ae0 Mon Sep 17 00:00:00 2001 From: Grant Nelson Date: Tue, 24 Sep 2024 11:56:48 -0600 Subject: [PATCH 1/2] Adding compiler tests for DCE --- compiler/compiler_test.go | 597 +++++++++++++++++++++++++++++++++++--- go.mod | 5 +- go.sum | 1 + 3 files changed, 554 insertions(+), 49 deletions(-) diff --git a/compiler/compiler_test.go b/compiler/compiler_test.go index 377f09d94..c17037e94 100644 --- a/compiler/compiler_test.go +++ b/compiler/compiler_test.go @@ -2,15 +2,17 @@ package compiler import ( "bytes" - "go/ast" - "go/build" - "go/parser" - "go/token" + "fmt" "go/types" + "path/filepath" + "regexp" + "sort" "testing" "github.com/google/go-cmp/cmp" - "golang.org/x/tools/go/loader" + "golang.org/x/tools/go/packages" + + "github.com/gopherjs/gopherjs/compiler/internal/dce" ) func TestOrder(t *testing.T) { @@ -41,27 +43,336 @@ func Bfunc() int { return varA+varB } ` + files := []source{{"fileA.go", []byte(fileA)}, {"fileB.go", []byte(fileB)}} - compare(t, "foo", files, false) - compare(t, "foo", files, true) + compareOrder(t, files, false) + compareOrder(t, files, true) } -func compare(t *testing.T, path string, sourceFiles []source, minify bool) { - outputNormal, err := compile(path, sourceFiles, minify) - if err != nil { - t.Fatal(err) - } +func TestDeclSelection_KeepUnusedExportedMethods(t *testing.T) { + src := ` + package main + type Foo struct {} + func (f Foo) Bar() { + println("bar") + } + func (f Foo) Baz() { // unused + println("baz") + } + func main() { + Foo{}.Bar() + }` + + srcFiles := []source{{`main.go`, []byte(src)}} + sel := declSelection(t, srcFiles, nil) + + sel.DeclCode.IsAlive(`^\s*Foo = \$newType`) + sel.DeclCode.IsAlive(`^\s*\$ptrType\(Foo\)\.prototype\.Bar`) + sel.DeclCode.IsAlive(`^\s*\$ptrType\(Foo\)\.prototype\.Baz`) +} + +func TestDeclSelection_RemoveUnusedUnexportedMethods(t *testing.T) { + src := ` + package main + type Foo struct {} + func (f Foo) Bar() { + println("bar") + } + func (f Foo) baz() { // unused + println("baz") + } + func main() { + Foo{}.Bar() + }` + + srcFiles := []source{{`main.go`, []byte(src)}} + sel := declSelection(t, srcFiles, nil) + + sel.DeclCode.IsAlive(`^\s*Foo = \$newType`) + sel.DeclCode.IsAlive(`^\s*\$ptrType\(Foo\)\.prototype\.Bar`) + + sel.DeclCode.IsDead(`^\s*\$ptrType\(Foo\)\.prototype\.baz`) +} + +func TestDeclSelection_KeepUnusedUnexportedMethodForInterface(t *testing.T) { + src := ` + package main + type Foo struct {} + func (f Foo) Bar() { + println("foo") + } + func (f Foo) baz() {} // unused + + type Foo2 struct {} + func (f Foo2) Bar() { + println("foo2") + } + + type IFoo interface { + Bar() + baz() + } + func main() { + fs := []any{ Foo{}, Foo2{} } + for _, f := range fs { + if i, ok := f.(IFoo); ok { + i.Bar() + } + } + }` + + srcFiles := []source{{`main.go`, []byte(src)}} + sel := declSelection(t, srcFiles, nil) + + sel.DeclCode.IsAlive(`^\s*Foo = \$newType`) + sel.DeclCode.IsAlive(`^\s*\$ptrType\(Foo\)\.prototype\.Bar`) + + // `baz` is used to duck-type (via method list) against IFoo + // but the method itself is not used so can be removed. + sel.DeclCode.IsDead(`^\s*\$ptrType\(Foo\)\.prototype\.baz`) + sel.MethodListCode.IsAlive(`^\s*Foo.methods = .* \{prop: "baz", name: "baz"`) +} + +func TestDeclSelection_KeepUnexportedMethodUsedViaInterfaceLit(t *testing.T) { + src := ` + package main + type Foo struct {} + func (f Foo) Bar() { + println("foo") + } + func (f Foo) baz() { + println("baz") + } + func main() { + var f interface { + Bar() + baz() + } = Foo{} + f.baz() + }` + + srcFiles := []source{{`main.go`, []byte(src)}} + sel := declSelection(t, srcFiles, nil) + + sel.DeclCode.IsAlive(`^\s*Foo = \$newType`) + sel.DeclCode.IsAlive(`^\s*\$ptrType\(Foo\)\.prototype\.Bar`) + sel.DeclCode.IsAlive(`^\s*\$ptrType\(Foo\)\.prototype\.baz`) +} + +func TestDeclSelection_KeepAliveUnexportedMethodsUsedInMethodExpressions(t *testing.T) { + src := ` + package main + type Foo struct {} + func (f Foo) baz() { + println("baz") + } + func main() { + fb := Foo.baz + fb(Foo{}) + }` + + srcFiles := []source{{`main.go`, []byte(src)}} + sel := declSelection(t, srcFiles, nil) + + sel.DeclCode.IsAlive(`^\s*Foo = \$newType`) + sel.DeclCode.IsAlive(`^\s*\$ptrType\(Foo\)\.prototype\.baz`) +} + +func TestDeclSelection_RemoveUnusedFuncInstance(t *testing.T) { + src := ` + package main + func Sum[T int | float64](values ...T) T { + var sum T + for _, v := range values { + sum += v + } + return sum + } + func Foo() { // unused + println(Sum(1, 2, 3)) + } + func main() { + println(Sum(1.1, 2.2, 3.3)) + }` + + srcFiles := []source{{`main.go`, []byte(src)}} + sel := declSelection(t, srcFiles, nil) + + sel.DeclCode.IsAlive(`^\s*Sum\[\d+ /\* float64 \*/\]`) + sel.DeclCode.IsAlive(`^\s*sliceType(\$\d+)? = \$sliceType\(\$Float64\)`) + + sel.DeclCode.IsDead(`^\s*Foo = function`) + sel.DeclCode.IsDead(`^\s*sliceType(\$\d+)? = \$sliceType\(\$Int\)`) + + // TODO(gn): This should not be alive because it is not used. + sel.DeclCode.IsAlive(`^\s*Sum\[\d+ /\* int \*/\]`) +} + +func TestDeclSelection_RemoveUnusedStructTypeInstances(t *testing.T) { + src := ` + package main + type Foo[T any] struct { v T } + func (f Foo[T]) Bar() { + println(f.v) + } + + var _ = Foo[float64]{v: 3.14} // unused + + func main() { + Foo[int]{v: 7}.Bar() + }` + + srcFiles := []source{{`main.go`, []byte(src)}} + sel := declSelection(t, srcFiles, nil) + + sel.DeclCode.IsAlive(`^\s*Foo\[\d+ /\* int \*/\] = \$newType`) + sel.DeclCode.IsAlive(`^\s*\$ptrType\(Foo\[\d+ /\* int \*/\]\)\.prototype\.Bar`) + + // TODO(gn): This should not be alive because it is not used. + sel.DeclCode.IsAlive(`^\s*Foo\[\d+ /\* float64 \*/\] = \$newType`) + sel.DeclCode.IsAlive(`^\s*\$ptrType\(Foo\[\d+ /\* float64 \*/\]\)\.prototype\.Bar`) +} + +func TestDeclSelection_RemoveUnusedInterfaceTypeInstances(t *testing.T) { + src := ` + package main + type Foo[T any] interface { Bar(v T) } + + type Baz int + func (b Baz) Bar(v int) { + println(v + int(b)) + } + + var F64 = FooBar[float64] // unused + + func FooBar[T any](f Foo[T], v T) { + f.Bar(v) + } + + func main() { + FooBar[int](Baz(42), 12) // Baz implements Foo[int] + }` + + srcFiles := []source{{`main.go`, []byte(src)}} + sel := declSelection(t, srcFiles, nil) + + sel.DeclCode.IsAlive(`^\s*Baz = \$newType`) + sel.DeclCode.IsAlive(`^\s*Baz\.prototype\.Bar`) + sel.InitCode.IsDead(`\$pkg\.F64 = FooBar\[\d+ /\* float64 \*/\]`) + + sel.DeclCode.IsAlive(`^\s*FooBar\[\d+ /\* int \*/\]`) + // TODO(gn): Below should be alive because it is an arg to FooBar[int]. + sel.DeclCode.IsDead(`^\s*Foo\[\d+ /\* int \*/\] = \$newType`) + + // TODO(gn): Below should be dead because it is only used by a dead init. + sel.DeclCode.IsAlive(`^\s*FooBar\[\d+ /\* float64 \*/\]`) + sel.DeclCode.IsDead(`^\s*Foo\[\d+ /\* float64 \*/\] = \$newType`) +} + +func TestDeclSelection_RemoveUnusedMethodWithDifferentSignature(t *testing.T) { + src := ` + package main + type Foo struct{} + func (f Foo) Bar() { println("Foo") } + func (f Foo) baz(x int) { println(x) } // unused + + type Foo2 struct{} + func (f Foo2) Bar() { println("Foo2") } + func (f Foo2) baz(x string) { println(x) } + + func main() { + f1 := Foo{} + f1.Bar() + + f2 := Foo2{} + f2.Bar() + f2.baz("foo") + }` + + srcFiles := []source{{`main.go`, []byte(src)}} + sel := declSelection(t, srcFiles, nil) + + sel.DeclCode.IsAlive(`^\s*Foo = \$newType`) + sel.DeclCode.IsAlive(`\s*\$ptrType\(Foo\)\.prototype\.Bar`) + // TODO(gn): Below should be dead because it is not used even though + // its name matches a used unexported method. + sel.DeclCode.IsAlive(`\s*\$ptrType\(Foo\)\.prototype\.baz`) + + sel.DeclCode.IsAlive(`^\s*Foo2 = \$newType`) + sel.DeclCode.IsAlive(`\s*\$ptrType\(Foo2\)\.prototype\.Bar`) + sel.DeclCode.IsAlive(`\s*\$ptrType\(Foo2\)\.prototype\.baz`) +} + +func TestDeclSelection_RemoveUnusedUnexportedMethodInstance(t *testing.T) { + src := ` + package main + type Foo[T any] struct{} + func (f Foo[T]) Bar() { println("Foo") } + func (f Foo[T]) baz(x T) { Baz[T]{v: x}.Bar() } + + type Baz[T any] struct{ v T } + func (b Baz[T]) Bar() { println("Baz", b.v) } + + func main() { + f1 := Foo[int]{} + f1.Bar() + f1.baz(7) + + f2 := Foo[uint]{} // Foo[uint].baz is unused + f2.Bar() + }` + + srcFiles := []source{{`main.go`, []byte(src)}} + sel := declSelection(t, srcFiles, nil) + + sel.DeclCode.IsAlive(`^\s*Foo\[\d+ /\* int \*/\] = \$newType`) + sel.DeclCode.IsAlive(`\s*\$ptrType\(Foo\[\d+ /\* int \*/\]\)\.prototype\.Bar`) + sel.DeclCode.IsAlive(`\s*\$ptrType\(Foo\[\d+ /\* int \*/\]\)\.prototype\.baz`) + sel.DeclCode.IsAlive(`^\s*Baz\[\d+ /\* int \*/\] = \$newType`) + sel.DeclCode.IsAlive(`\s*\$ptrType\(Baz\[\d+ /\* int \*/\]\)\.prototype\.Bar`) + + sel.DeclCode.IsAlive(`^\s*Foo\[\d+ /\* uint \*/\] = \$newType`) + sel.DeclCode.IsAlive(`\s*\$ptrType\(Foo\[\d+ /\* uint \*/\]\)\.prototype\.Bar`) + // TODO(gn): All three below should be dead because Foo[uint].baz is unused. + sel.DeclCode.IsAlive(`\s*\$ptrType\(Foo\[\d+ /\* uint \*/\]\)\.prototype\.baz`) + sel.DeclCode.IsAlive(`^\s*Baz\[\d+ /\* uint \*/\] = \$newType`) + sel.DeclCode.IsAlive(`\s*\$ptrType\(Baz\[\d+ /\* uint \*/\]\)\.prototype\.Bar`) +} + +func TestDeclSelection_RemoveUnusedTypeConstraint(t *testing.T) { + src := ` + package main + type Foo interface{ int | string } + + type Bar[T Foo] struct{ v T } + func (b Bar[T]) Baz() { println(b.v) } + + var ghost = Bar[int]{v: 7} // unused + + func main() { + println("do nothing") + }` + + srcFiles := []source{{`main.go`, []byte(src)}} + sel := declSelection(t, srcFiles, nil) + + sel.DeclCode.IsDead(`^\s*Foo = \$newType`) + sel.DeclCode.IsDead(`^\s*Bar\[\d+ /\* int \*/\] = \$newType`) + sel.DeclCode.IsDead(`^\s*\$ptrType\(Bar\[\d+ /\* int \*/\]\)\.prototype\.Baz`) + sel.InitCode.IsDead(`ghost = new Bar\[\d+ /\* int \*/\]\.ptr\(7\)`) +} + +func compareOrder(t *testing.T, sourceFiles []source, minify bool) { + t.Helper() + outputNormal := compile(t, sourceFiles, minify) // reverse the array for i, j := 0, len(sourceFiles)-1; i < j; i, j = i+1, j-1 { sourceFiles[i], sourceFiles[j] = sourceFiles[j], sourceFiles[i] } - outputReversed, err := compile(path, sourceFiles, minify) - if err != nil { - t.Fatal(err) - } + outputReversed := compile(t, sourceFiles, minify) if diff := cmp.Diff(string(outputNormal), string(outputReversed)); diff != "" { t.Errorf("files in different order produce different JS:\n%s", diff) @@ -73,44 +384,116 @@ type source struct { contents []byte } -func compile(path string, sourceFiles []source, minify bool) ([]byte, error) { - conf := loader.Config{} - conf.Fset = token.NewFileSet() - conf.ParserMode = parser.ParseComments +func compile(t *testing.T, sourceFiles []source, minify bool) []byte { + t.Helper() + rootPkg := parseSources(t, sourceFiles, nil) + archives := compileProject(t, rootPkg, minify) - context := build.Default // make a copy of build.Default - conf.Build = &context - conf.Build.BuildTags = []string{"js"} + path := rootPkg.PkgPath + a, ok := archives[path] + if !ok { + t.Fatalf(`root package not found in archives: %s`, path) + } - var astFiles []*ast.File - for _, sourceFile := range sourceFiles { - astFile, err := parser.ParseFile(conf.Fset, sourceFile.name, sourceFile.contents, parser.ParseComments) - if err != nil { - return nil, err - } - astFiles = append(astFiles, astFile) + b := renderPackage(t, a, minify) + if len(b) == 0 { + t.Fatal(`compile had no output`) } - conf.CreateFromFiles(path, astFiles...) - prog, err := conf.Load() + return b +} + +// parseSources parses the given source files and returns the root package +// that contains the given source files. +// +// The source file should all be from the same package as the files for the +// root package. At least one source file must be given. +// +// The auxillary files can be for different packages but should have paths +// added to the source name so that they can be grouped together by package. +// To import an auxillary package, the path should be prepended by +// `github.com/gopherjs/gopherjs/compiler`. +func parseSources(t *testing.T, sourceFiles []source, auxFiles []source) *packages.Package { + t.Helper() + const mode = packages.NeedName | + packages.NeedFiles | + packages.NeedImports | + packages.NeedDeps | + packages.NeedTypes | + packages.NeedSyntax + + dir, err := filepath.Abs(`./`) if err != nil { - return nil, err + t.Fatal(`error getting working directory:`, err) } + patterns := make([]string, len(sourceFiles)) + overlay := make(map[string][]byte, len(sourceFiles)) + for i, src := range sourceFiles { + filename := src.name + patterns[i] = filename + absName := filepath.Join(dir, filename) + overlay[absName] = []byte(src.contents) + } + for _, src := range auxFiles { + absName := filepath.Join(dir, src.name) + overlay[absName] = []byte(src.contents) + } + + config := &packages.Config{ + Mode: mode, + Overlay: overlay, + Dir: dir, + } + + pkgs, err := packages.Load(config, patterns...) + if err != nil { + t.Fatal(`error loading packages:`, err) + } + + hasErrors := false + packages.Visit(pkgs, nil, func(pkg *packages.Package) { + for _, err := range pkg.Errors { + hasErrors = true + fmt.Println(err) + } + }) + if hasErrors { + t.FailNow() + } + + if len(pkgs) != 1 { + t.Fatal(`expected one and only one root package but got`, len(pkgs)) + } + return pkgs[0] +} + +// compileProject compiles the given root package and all packages imported by the root. +// This returns the compiled archives of all packages keyed by their import path. +func compileProject(t *testing.T, root *packages.Package, minify bool) map[string]*Archive { + t.Helper() + pkgMap := map[string]*packages.Package{} + packages.Visit([]*packages.Package{root}, nil, func(pkg *packages.Package) { + pkgMap[pkg.PkgPath] = pkg + }) + archiveCache := map[string]*Archive{} var importContext *ImportContext importContext = &ImportContext{ - Packages: make(map[string]*types.Package), + Packages: map[string]*types.Package{}, Import: func(path string) (*Archive, error) { // find in local cache if a, ok := archiveCache[path]; ok { return a, nil } - pi := prog.Package(path) - importContext.Packages[path] = pi.Pkg + pkg, ok := pkgMap[path] + if !ok { + t.Fatal(`package not found:`, path) + } + importContext.Packages[path] = pkg.Types // compile package - a, err := Compile(path, pi.Files, prog.Fset, importContext, minify) + a, err := Compile(path, pkg.Syntax, pkg.Fset, importContext, minify) if err != nil { return nil, err } @@ -119,18 +502,15 @@ func compile(path string, sourceFiles []source, minify bool) ([]byte, error) { }, } - a, err := importContext.Import(path) - if err != nil { - return nil, err - } - b, err := renderPackage(a) + _, err := importContext.Import(root.PkgPath) if err != nil { - return nil, err + t.Fatal(`failed to compile:`, err) } - return b, nil + return archiveCache } -func renderPackage(archive *Archive) ([]byte, error) { +func renderPackage(t *testing.T, archive *Archive, minify bool) []byte { + t.Helper() selection := make(map[*Decl]struct{}) for _, d := range archive.Declarations { selection[d] = struct{}{} @@ -138,9 +518,130 @@ func renderPackage(archive *Archive) ([]byte, error) { buf := &bytes.Buffer{} - if err := WritePkgCode(archive, selection, goLinknameSet{}, false, &SourceMapFilter{Writer: buf}); err != nil { - return nil, err + if err := WritePkgCode(archive, selection, goLinknameSet{}, minify, &SourceMapFilter{Writer: buf}); err != nil { + t.Fatal(err) } - return buf.Bytes(), nil + return buf.Bytes() +} + +type selectionTester struct { + t *testing.T + mainPkg *Archive + archives map[string]*Archive + packages []*Archive + dceSelection map[*Decl]struct{} + + DeclCode *selectionCodeTester + InitCode *selectionCodeTester + MethodListCode *selectionCodeTester +} + +func declSelection(t *testing.T, sourceFiles []source, auxFiles []source) *selectionTester { + t.Helper() + root := parseSources(t, sourceFiles, auxFiles) + archives := compileProject(t, root, false) + mainPkg := archives[root.PkgPath] + + paths := make([]string, 0, len(archives)) + for path := range archives { + paths = append(paths, path) + } + sort.Strings(paths) + packages := make([]*Archive, 0, len(archives)-1) + for _, path := range paths { + packages = append(packages, archives[path]) + } + + sel := &dce.Selector[*Decl]{} + for _, pkg := range packages { + for _, d := range pkg.Declarations { + sel.Include(d, false) + } + } + dceSelection := sel.AliveDecls() + + st := &selectionTester{ + t: t, + mainPkg: mainPkg, + archives: archives, + packages: packages, + dceSelection: dceSelection, + } + + st.DeclCode = &selectionCodeTester{st, `DeclCode`, func(d *Decl) []byte { return d.DeclCode }} + st.InitCode = &selectionCodeTester{st, `InitCode`, func(d *Decl) []byte { return d.InitCode }} + st.MethodListCode = &selectionCodeTester{st, `MethodListCode`, func(d *Decl) []byte { return d.MethodListCode }} + return st +} + +func (st *selectionTester) PrintDeclStatus() { + st.t.Helper() + for _, pkg := range st.packages { + fmt.Println(`Package`, pkg.ImportPath) + for _, decl := range pkg.Declarations { + if _, ok := st.dceSelection[decl]; ok { + fmt.Printf(" [Alive] %q\n", string(decl.FullName)) + } else { + fmt.Printf(" [Dead] %q\n", string(decl.FullName)) + } + if len(decl.DeclCode) > 0 { + fmt.Printf(" DeclCode: %q\n", string(decl.DeclCode)) + } + if len(decl.InitCode) > 0 { + fmt.Printf(" InitCode: %q\n", string(decl.InitCode)) + } + if len(decl.MethodListCode) > 0 { + fmt.Printf(" MethodListCode: %q\n", string(decl.MethodListCode)) + } + if len(decl.TypeInitCode) > 0 { + fmt.Printf(" TypeInitCode: %q\n", string(decl.TypeInitCode)) + } + if len(decl.Vars) > 0 { + fmt.Println(` Vars:`, decl.Vars) + } + } + } +} + +type selectionCodeTester struct { + st *selectionTester + codeName string + getCode func(*Decl) []byte +} + +func (ct *selectionCodeTester) IsAlive(pattern string) { + ct.st.t.Helper() + decl := ct.FindDeclMatch(pattern) + if _, ok := ct.st.dceSelection[decl]; !ok { + ct.st.t.Error(`expected the`, ct.codeName, `code to be alive:`, pattern) + } +} + +func (ct *selectionCodeTester) IsDead(pattern string) { + ct.st.t.Helper() + decl := ct.FindDeclMatch(pattern) + if _, ok := ct.st.dceSelection[decl]; ok { + ct.st.t.Error(`expected the`, ct.codeName, `code to be dead:`, pattern) + } +} + +func (ct *selectionCodeTester) FindDeclMatch(pattern string) *Decl { + ct.st.t.Helper() + regex := regexp.MustCompile(pattern) + var found *Decl + for _, pkg := range ct.st.packages { + for _, d := range pkg.Declarations { + if regex.Match(ct.getCode(d)) { + if found != nil { + ct.st.t.Fatal(`multiple`, ct.codeName, `found containing pattern:`, pattern) + } + found = d + } + } + } + if found == nil { + ct.st.t.Fatal(ct.codeName, `not found with pattern:`, pattern) + } + return found } diff --git a/go.mod b/go.mod index ccb130f48..cfa813b63 100644 --- a/go.mod +++ b/go.mod @@ -20,4 +20,7 @@ require ( golang.org/x/tools v0.16.0 ) -require github.com/inconshreveable/mousetrap v1.0.0 // indirect +require ( + github.com/inconshreveable/mousetrap v1.0.0 // indirect + golang.org/x/mod v0.14.0 // indirect +) diff --git a/go.sum b/go.sum index 8e69980d0..65b1d6a2c 100644 --- a/go.sum +++ b/go.sum @@ -298,6 +298,7 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= From 2539cd522ce100850aa1871daac8bc1bfabaa7b2 Mon Sep 17 00:00:00 2001 From: Grant Nelson Date: Wed, 9 Oct 2024 17:16:20 -0600 Subject: [PATCH 2/2] making requested changes to compiler tests --- compiler/compiler_test.go | 234 ++++++++++++------------------ internal/srctesting/srctesting.go | 74 ++++++++++ 2 files changed, 165 insertions(+), 143 deletions(-) diff --git a/compiler/compiler_test.go b/compiler/compiler_test.go index c17037e94..5276cbea5 100644 --- a/compiler/compiler_test.go +++ b/compiler/compiler_test.go @@ -2,9 +2,7 @@ package compiler import ( "bytes" - "fmt" "go/types" - "path/filepath" "regexp" "sort" "testing" @@ -13,38 +11,40 @@ import ( "golang.org/x/tools/go/packages" "github.com/gopherjs/gopherjs/compiler/internal/dce" + "github.com/gopherjs/gopherjs/internal/srctesting" ) func TestOrder(t *testing.T) { fileA := ` -package foo + package foo -var Avar = "a" + var Avar = "a" -type Atype struct{} + type Atype struct{} -func Afunc() int { - var varA = 1 - var varB = 2 - return varA+varB -} -` + func Afunc() int { + var varA = 1 + var varB = 2 + return varA+varB + }` fileB := ` -package foo + package foo -var Bvar = "b" + var Bvar = "b" -type Btype struct{} + type Btype struct{} -func Bfunc() int { - var varA = 1 - var varB = 2 - return varA+varB -} -` + func Bfunc() int { + var varA = 1 + var varB = 2 + return varA+varB + }` - files := []source{{"fileA.go", []byte(fileA)}, {"fileB.go", []byte(fileB)}} + files := []srctesting.Source{ + {Name: "fileA.go", Contents: []byte(fileA)}, + {Name: "fileB.go", Contents: []byte(fileB)}, + } compareOrder(t, files, false) compareOrder(t, files, true) @@ -64,7 +64,7 @@ func TestDeclSelection_KeepUnusedExportedMethods(t *testing.T) { Foo{}.Bar() }` - srcFiles := []source{{`main.go`, []byte(src)}} + srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} sel := declSelection(t, srcFiles, nil) sel.DeclCode.IsAlive(`^\s*Foo = \$newType`) @@ -86,7 +86,7 @@ func TestDeclSelection_RemoveUnusedUnexportedMethods(t *testing.T) { Foo{}.Bar() }` - srcFiles := []source{{`main.go`, []byte(src)}} + srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} sel := declSelection(t, srcFiles, nil) sel.DeclCode.IsAlive(`^\s*Foo = \$newType`) @@ -122,14 +122,14 @@ func TestDeclSelection_KeepUnusedUnexportedMethodForInterface(t *testing.T) { } }` - srcFiles := []source{{`main.go`, []byte(src)}} + srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} sel := declSelection(t, srcFiles, nil) sel.DeclCode.IsAlive(`^\s*Foo = \$newType`) sel.DeclCode.IsAlive(`^\s*\$ptrType\(Foo\)\.prototype\.Bar`) - // `baz` is used to duck-type (via method list) against IFoo - // but the method itself is not used so can be removed. + // `baz` signature metadata is used to check a type assertion against IFoo, + // but the method itself is never called, so it can be removed. sel.DeclCode.IsDead(`^\s*\$ptrType\(Foo\)\.prototype\.baz`) sel.MethodListCode.IsAlive(`^\s*Foo.methods = .* \{prop: "baz", name: "baz"`) } @@ -152,7 +152,7 @@ func TestDeclSelection_KeepUnexportedMethodUsedViaInterfaceLit(t *testing.T) { f.baz() }` - srcFiles := []source{{`main.go`, []byte(src)}} + srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} sel := declSelection(t, srcFiles, nil) sel.DeclCode.IsAlive(`^\s*Foo = \$newType`) @@ -172,7 +172,7 @@ func TestDeclSelection_KeepAliveUnexportedMethodsUsedInMethodExpressions(t *test fb(Foo{}) }` - srcFiles := []source{{`main.go`, []byte(src)}} + srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} sel := declSelection(t, srcFiles, nil) sel.DeclCode.IsAlive(`^\s*Foo = \$newType`) @@ -196,7 +196,7 @@ func TestDeclSelection_RemoveUnusedFuncInstance(t *testing.T) { println(Sum(1.1, 2.2, 3.3)) }` - srcFiles := []source{{`main.go`, []byte(src)}} + srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} sel := declSelection(t, srcFiles, nil) sel.DeclCode.IsAlive(`^\s*Sum\[\d+ /\* float64 \*/\]`) @@ -205,7 +205,7 @@ func TestDeclSelection_RemoveUnusedFuncInstance(t *testing.T) { sel.DeclCode.IsDead(`^\s*Foo = function`) sel.DeclCode.IsDead(`^\s*sliceType(\$\d+)? = \$sliceType\(\$Int\)`) - // TODO(gn): This should not be alive because it is not used. + // TODO(grantnelson-wf): This should not be alive because it is not used. sel.DeclCode.IsAlive(`^\s*Sum\[\d+ /\* int \*/\]`) } @@ -223,13 +223,13 @@ func TestDeclSelection_RemoveUnusedStructTypeInstances(t *testing.T) { Foo[int]{v: 7}.Bar() }` - srcFiles := []source{{`main.go`, []byte(src)}} + srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} sel := declSelection(t, srcFiles, nil) sel.DeclCode.IsAlive(`^\s*Foo\[\d+ /\* int \*/\] = \$newType`) sel.DeclCode.IsAlive(`^\s*\$ptrType\(Foo\[\d+ /\* int \*/\]\)\.prototype\.Bar`) - // TODO(gn): This should not be alive because it is not used. + // TODO(grantnelson-wf): This should not be alive because it is not used. sel.DeclCode.IsAlive(`^\s*Foo\[\d+ /\* float64 \*/\] = \$newType`) sel.DeclCode.IsAlive(`^\s*\$ptrType\(Foo\[\d+ /\* float64 \*/\]\)\.prototype\.Bar`) } @@ -254,7 +254,7 @@ func TestDeclSelection_RemoveUnusedInterfaceTypeInstances(t *testing.T) { FooBar[int](Baz(42), 12) // Baz implements Foo[int] }` - srcFiles := []source{{`main.go`, []byte(src)}} + srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} sel := declSelection(t, srcFiles, nil) sel.DeclCode.IsAlive(`^\s*Baz = \$newType`) @@ -262,10 +262,10 @@ func TestDeclSelection_RemoveUnusedInterfaceTypeInstances(t *testing.T) { sel.InitCode.IsDead(`\$pkg\.F64 = FooBar\[\d+ /\* float64 \*/\]`) sel.DeclCode.IsAlive(`^\s*FooBar\[\d+ /\* int \*/\]`) - // TODO(gn): Below should be alive because it is an arg to FooBar[int]. + // TODO(grantnelson-wf): Below should be alive because it is an arg to FooBar[int]. sel.DeclCode.IsDead(`^\s*Foo\[\d+ /\* int \*/\] = \$newType`) - // TODO(gn): Below should be dead because it is only used by a dead init. + // TODO(grantnelson-wf): Below should be dead because it is only used by a dead init. sel.DeclCode.IsAlive(`^\s*FooBar\[\d+ /\* float64 \*/\]`) sel.DeclCode.IsDead(`^\s*Foo\[\d+ /\* float64 \*/\] = \$newType`) } @@ -290,12 +290,12 @@ func TestDeclSelection_RemoveUnusedMethodWithDifferentSignature(t *testing.T) { f2.baz("foo") }` - srcFiles := []source{{`main.go`, []byte(src)}} + srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} sel := declSelection(t, srcFiles, nil) sel.DeclCode.IsAlive(`^\s*Foo = \$newType`) sel.DeclCode.IsAlive(`\s*\$ptrType\(Foo\)\.prototype\.Bar`) - // TODO(gn): Below should be dead because it is not used even though + // TODO(grantnelson-wf): Below should be dead because it is not used even though // its name matches a used unexported method. sel.DeclCode.IsAlive(`\s*\$ptrType\(Foo\)\.prototype\.baz`) @@ -323,7 +323,7 @@ func TestDeclSelection_RemoveUnusedUnexportedMethodInstance(t *testing.T) { f2.Bar() }` - srcFiles := []source{{`main.go`, []byte(src)}} + srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} sel := declSelection(t, srcFiles, nil) sel.DeclCode.IsAlive(`^\s*Foo\[\d+ /\* int \*/\] = \$newType`) @@ -334,7 +334,7 @@ func TestDeclSelection_RemoveUnusedUnexportedMethodInstance(t *testing.T) { sel.DeclCode.IsAlive(`^\s*Foo\[\d+ /\* uint \*/\] = \$newType`) sel.DeclCode.IsAlive(`\s*\$ptrType\(Foo\[\d+ /\* uint \*/\]\)\.prototype\.Bar`) - // TODO(gn): All three below should be dead because Foo[uint].baz is unused. + // TODO(grantnelson-wf): All three below should be dead because Foo[uint].baz is unused. sel.DeclCode.IsAlive(`\s*\$ptrType\(Foo\[\d+ /\* uint \*/\]\)\.prototype\.baz`) sel.DeclCode.IsAlive(`^\s*Baz\[\d+ /\* uint \*/\] = \$newType`) sel.DeclCode.IsAlive(`\s*\$ptrType\(Baz\[\d+ /\* uint \*/\]\)\.prototype\.Bar`) @@ -354,7 +354,7 @@ func TestDeclSelection_RemoveUnusedTypeConstraint(t *testing.T) { println("do nothing") }` - srcFiles := []source{{`main.go`, []byte(src)}} + srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}} sel := declSelection(t, srcFiles, nil) sel.DeclCode.IsDead(`^\s*Foo = \$newType`) @@ -363,7 +363,7 @@ func TestDeclSelection_RemoveUnusedTypeConstraint(t *testing.T) { sel.InitCode.IsDead(`ghost = new Bar\[\d+ /\* int \*/\]\.ptr\(7\)`) } -func compareOrder(t *testing.T, sourceFiles []source, minify bool) { +func compareOrder(t *testing.T, sourceFiles []srctesting.Source, minify bool) { t.Helper() outputNormal := compile(t, sourceFiles, minify) @@ -379,14 +379,9 @@ func compareOrder(t *testing.T, sourceFiles []source, minify bool) { } } -type source struct { - name string - contents []byte -} - -func compile(t *testing.T, sourceFiles []source, minify bool) []byte { +func compile(t *testing.T, sourceFiles []srctesting.Source, minify bool) []byte { t.Helper() - rootPkg := parseSources(t, sourceFiles, nil) + rootPkg := srctesting.ParseSources(t, sourceFiles, nil) archives := compileProject(t, rootPkg, minify) path := rootPkg.PkgPath @@ -402,71 +397,6 @@ func compile(t *testing.T, sourceFiles []source, minify bool) []byte { return b } -// parseSources parses the given source files and returns the root package -// that contains the given source files. -// -// The source file should all be from the same package as the files for the -// root package. At least one source file must be given. -// -// The auxillary files can be for different packages but should have paths -// added to the source name so that they can be grouped together by package. -// To import an auxillary package, the path should be prepended by -// `github.com/gopherjs/gopherjs/compiler`. -func parseSources(t *testing.T, sourceFiles []source, auxFiles []source) *packages.Package { - t.Helper() - const mode = packages.NeedName | - packages.NeedFiles | - packages.NeedImports | - packages.NeedDeps | - packages.NeedTypes | - packages.NeedSyntax - - dir, err := filepath.Abs(`./`) - if err != nil { - t.Fatal(`error getting working directory:`, err) - } - - patterns := make([]string, len(sourceFiles)) - overlay := make(map[string][]byte, len(sourceFiles)) - for i, src := range sourceFiles { - filename := src.name - patterns[i] = filename - absName := filepath.Join(dir, filename) - overlay[absName] = []byte(src.contents) - } - for _, src := range auxFiles { - absName := filepath.Join(dir, src.name) - overlay[absName] = []byte(src.contents) - } - - config := &packages.Config{ - Mode: mode, - Overlay: overlay, - Dir: dir, - } - - pkgs, err := packages.Load(config, patterns...) - if err != nil { - t.Fatal(`error loading packages:`, err) - } - - hasErrors := false - packages.Visit(pkgs, nil, func(pkg *packages.Package) { - for _, err := range pkg.Errors { - hasErrors = true - fmt.Println(err) - } - }) - if hasErrors { - t.FailNow() - } - - if len(pkgs) != 1 { - t.Fatal(`expected one and only one root package but got`, len(pkgs)) - } - return pkgs[0] -} - // compileProject compiles the given root package and all packages imported by the root. // This returns the compiled archives of all packages keyed by their import path. func compileProject(t *testing.T, root *packages.Package, minify bool) map[string]*Archive { @@ -537,9 +467,9 @@ type selectionTester struct { MethodListCode *selectionCodeTester } -func declSelection(t *testing.T, sourceFiles []source, auxFiles []source) *selectionTester { +func declSelection(t *testing.T, sourceFiles []srctesting.Source, auxFiles []srctesting.Source) *selectionTester { t.Helper() - root := parseSources(t, sourceFiles, auxFiles) + root := srctesting.ParseSources(t, sourceFiles, auxFiles) archives := compileProject(t, root, false) mainPkg := archives[root.PkgPath] @@ -548,7 +478,7 @@ func declSelection(t *testing.T, sourceFiles []source, auxFiles []source) *selec paths = append(paths, path) } sort.Strings(paths) - packages := make([]*Archive, 0, len(archives)-1) + packages := make([]*Archive, 0, len(archives)) for _, path := range paths { packages = append(packages, archives[path]) } @@ -561,87 +491,105 @@ func declSelection(t *testing.T, sourceFiles []source, auxFiles []source) *selec } dceSelection := sel.AliveDecls() - st := &selectionTester{ + return &selectionTester{ t: t, mainPkg: mainPkg, archives: archives, packages: packages, dceSelection: dceSelection, + DeclCode: &selectionCodeTester{ + t: t, + packages: packages, + dceSelection: dceSelection, + codeName: `DeclCode`, + getCode: func(d *Decl) []byte { return d.DeclCode }, + }, + InitCode: &selectionCodeTester{ + t: t, + packages: packages, + dceSelection: dceSelection, + codeName: `InitCode`, + getCode: func(d *Decl) []byte { return d.InitCode }, + }, + MethodListCode: &selectionCodeTester{ + t: t, + packages: packages, + dceSelection: dceSelection, + codeName: `MethodListCode`, + getCode: func(d *Decl) []byte { return d.MethodListCode }, + }, } - - st.DeclCode = &selectionCodeTester{st, `DeclCode`, func(d *Decl) []byte { return d.DeclCode }} - st.InitCode = &selectionCodeTester{st, `InitCode`, func(d *Decl) []byte { return d.InitCode }} - st.MethodListCode = &selectionCodeTester{st, `MethodListCode`, func(d *Decl) []byte { return d.MethodListCode }} - return st } func (st *selectionTester) PrintDeclStatus() { st.t.Helper() for _, pkg := range st.packages { - fmt.Println(`Package`, pkg.ImportPath) + st.t.Logf(`Package %s`, pkg.ImportPath) for _, decl := range pkg.Declarations { if _, ok := st.dceSelection[decl]; ok { - fmt.Printf(" [Alive] %q\n", string(decl.FullName)) + st.t.Logf(` [Alive] %q`, decl.FullName) } else { - fmt.Printf(" [Dead] %q\n", string(decl.FullName)) + st.t.Logf(` [Dead] %q`, decl.FullName) } if len(decl.DeclCode) > 0 { - fmt.Printf(" DeclCode: %q\n", string(decl.DeclCode)) + st.t.Logf(` DeclCode: %q`, string(decl.DeclCode)) } if len(decl.InitCode) > 0 { - fmt.Printf(" InitCode: %q\n", string(decl.InitCode)) + st.t.Logf(` InitCode: %q`, string(decl.InitCode)) } if len(decl.MethodListCode) > 0 { - fmt.Printf(" MethodListCode: %q\n", string(decl.MethodListCode)) + st.t.Logf(` MethodListCode: %q`, string(decl.MethodListCode)) } if len(decl.TypeInitCode) > 0 { - fmt.Printf(" TypeInitCode: %q\n", string(decl.TypeInitCode)) + st.t.Logf(` TypeInitCode: %q`, string(decl.TypeInitCode)) } if len(decl.Vars) > 0 { - fmt.Println(` Vars:`, decl.Vars) + st.t.Logf(` Vars: %v`, decl.Vars) } } } } type selectionCodeTester struct { - st *selectionTester - codeName string - getCode func(*Decl) []byte + t *testing.T + packages []*Archive + dceSelection map[*Decl]struct{} + codeName string + getCode func(*Decl) []byte } func (ct *selectionCodeTester) IsAlive(pattern string) { - ct.st.t.Helper() + ct.t.Helper() decl := ct.FindDeclMatch(pattern) - if _, ok := ct.st.dceSelection[decl]; !ok { - ct.st.t.Error(`expected the`, ct.codeName, `code to be alive:`, pattern) + if _, ok := ct.dceSelection[decl]; !ok { + ct.t.Error(`expected the`, ct.codeName, `code to be alive:`, pattern) } } func (ct *selectionCodeTester) IsDead(pattern string) { - ct.st.t.Helper() + ct.t.Helper() decl := ct.FindDeclMatch(pattern) - if _, ok := ct.st.dceSelection[decl]; ok { - ct.st.t.Error(`expected the`, ct.codeName, `code to be dead:`, pattern) + if _, ok := ct.dceSelection[decl]; ok { + ct.t.Error(`expected the`, ct.codeName, `code to be dead:`, pattern) } } func (ct *selectionCodeTester) FindDeclMatch(pattern string) *Decl { - ct.st.t.Helper() + ct.t.Helper() regex := regexp.MustCompile(pattern) var found *Decl - for _, pkg := range ct.st.packages { + for _, pkg := range ct.packages { for _, d := range pkg.Declarations { if regex.Match(ct.getCode(d)) { if found != nil { - ct.st.t.Fatal(`multiple`, ct.codeName, `found containing pattern:`, pattern) + ct.t.Fatal(`multiple`, ct.codeName, `found containing pattern:`, pattern) } found = d } } } if found == nil { - ct.st.t.Fatal(ct.codeName, `not found with pattern:`, pattern) + ct.t.Fatal(ct.codeName, `not found with pattern:`, pattern) } return found } diff --git a/internal/srctesting/srctesting.go b/internal/srctesting/srctesting.go index a74d31958..961dffd0b 100644 --- a/internal/srctesting/srctesting.go +++ b/internal/srctesting/srctesting.go @@ -10,8 +10,11 @@ import ( "go/parser" "go/token" "go/types" + "path/filepath" "strings" "testing" + + "golang.org/x/tools/go/packages" ) // Fixture provides utilities for parsing and type checking Go code in tests. @@ -171,3 +174,74 @@ func LookupObj(pkg *types.Package, name string) types.Object { } return obj } + +type Source struct { + Name string + Contents []byte +} + +// ParseSources parses the given source files and returns the root package +// that contains the given source files. +// +// The source file should all be from the same package as the files for the +// root package. At least one source file must be given. +// The root package's path will be `command-line-arguments`. +// +// The auxillary files can be for different packages but should have paths +// added to the source name so that they can be grouped together by package. +// To import an auxillary package, the path should be prepended by +// `github.com/gopherjs/gopherjs/compiler`. +func ParseSources(t *testing.T, sourceFiles []Source, auxFiles []Source) *packages.Package { + t.Helper() + const mode = packages.NeedName | + packages.NeedFiles | + packages.NeedImports | + packages.NeedDeps | + packages.NeedTypes | + packages.NeedSyntax + + dir, err := filepath.Abs(`./`) + if err != nil { + t.Fatal(`error getting working directory:`, err) + } + + patterns := make([]string, len(sourceFiles)) + overlay := make(map[string][]byte, len(sourceFiles)) + for i, src := range sourceFiles { + filename := src.Name + patterns[i] = filename + absName := filepath.Join(dir, filename) + overlay[absName] = []byte(src.Contents) + } + for _, src := range auxFiles { + absName := filepath.Join(dir, src.Name) + overlay[absName] = []byte(src.Contents) + } + + config := &packages.Config{ + Mode: mode, + Overlay: overlay, + Dir: dir, + } + + pkgs, err := packages.Load(config, patterns...) + if err != nil { + t.Fatal(`error loading packages:`, err) + } + + hasErrors := false + packages.Visit(pkgs, nil, func(pkg *packages.Package) { + for _, err := range pkg.Errors { + hasErrors = true + t.Error(err) + } + }) + if hasErrors { + t.FailNow() + } + + if len(pkgs) != 1 { + t.Fatal(`expected one and only one root package but got`, len(pkgs)) + } + return pkgs[0] +}