Adding compiler tests for DCE
diff --git a/compiler/compiler_test.go b/compiler/compiler_test.go
index 377f09d..c17037e 100644
--- a/compiler/compiler_test.go
+++ b/compiler/compiler_test.go
@@ -2,15 +2,17 @@
 
 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 @@
 	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 @@
 	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"}
-
-	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)
+	path := rootPkg.PkgPath
+	a, ok := archives[path]
+	if !ok {
+		t.Fatalf(`root package not found in archives: %s`, path)
 	}
-	conf.CreateFromFiles(path, astFiles...)
-	prog, err := conf.Load()
+
+	b := renderPackage(t, a, minify)
+	if len(b) == 0 {
+		t.Fatal(`compile had no output`)
+	}
+	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 @@
 		},
 	}
 
-	a, err := importContext.Import(path)
+	_, err := importContext.Import(root.PkgPath)
 	if err != nil {
-		return nil, err
+		t.Fatal(`failed to compile:`, err)
 	}
-	b, err := renderPackage(a)
-	if err != nil {
-		return nil, 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 @@
 
 	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 ccb130f..cfa813b 100644
--- a/go.mod
+++ b/go.mod
@@ -20,4 +20,7 @@
 	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 8e69980..65b1d6a 100644
--- a/go.sum
+++ b/go.sum
@@ -298,6 +298,7 @@
 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=