Repository: google/go-flow-levee
Pull request #164 (2020-10-30 23:09:08)

Change config interface to take basic types.
* Add utils methods to massage consumption of `string`-focused arguments.

- [x] Tests pass
- [x] Running against a large codebase such as [Kubernetes](https://github.com/kubernetes/kubernetes) does not error out.
- [n/a] Appropriate changes to README are included in PR

----

Coming from discussion in #157, this is a refactor in the direction of better modularity - SSA logic should be owned by analyzers and string-matching logic should belong to `config`.


Patch:
diff --git a/.travis.yml b/.travis.yml
index be90261..7588e10 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -35,4 +35,13 @@ script:
       false
     fi
   )
+- go install honnef.co/go/tools/cmd/staticcheck
+- >
+  echo "Checking staticcheck..." && (
+    if [[ -n $(staticcheck ./... 2>&1) ]]; then
+      staticcheck ./...
+      echo 'Please run `staticcheck` and fix the issues.'
+      false
+    fi
+  )
 - echo "Checking yaml..." && find . -name '*.yaml' -exec yamllint {} + &>/dev/null
diff --git a/internal/pkg/config/config.go b/internal/pkg/config/config.go
index 2e85bfb..e9305f1 100644
--- a/internal/pkg/config/config.go
+++ b/internal/pkg/config/config.go
@@ -17,7 +17,6 @@ package config
 import (
 	"flag"
 	"fmt"
-	"go/types"
 	"io/ioutil"
 	"reflect"
 	"strconv"
@@ -27,8 +26,6 @@ import (
 	"sigs.k8s.io/yaml"
 
 	"github.com/google/go-flow-levee/internal/pkg/config/regexp"
-	"github.com/google/go-flow-levee/internal/pkg/utils"
-	"golang.org/x/tools/go/ssa"
 )
 
 // FlagSet should be used by analyzers to reuse -config flag.
@@ -78,93 +75,45 @@ func (c Config) IsSourceFieldTag(tag string) bool {
 }
 
 // IsExcluded determines if a function matches one of the exclusion patterns.
-func (c Config) IsExcluded(fn *ssa.Function) bool {
+func (c Config) IsExcluded(path, recv, name string) bool {
 	for _, pm := range c.Exclude {
-		if pm.Match(fn) {
+		if pm.MatchFunction(path, recv, name) {
 			return true
 		}
 	}
 	return false
 }
 
-func (c Config) IsSinkCall(call *ssa.Call) bool {
-	callee := call.Call.StaticCallee()
-	if callee == nil {
-		return false
-	}
-	return c.IsSinkFunction(callee)
-}
-
-func (c Config) IsSinkFunction(f *ssa.Function) bool {
-	// according to the documentation for ssa.Function, this can happen if
-	// f is a "shared func", e.g. "wrappers and error.Error"
-	if f.Pkg == nil {
-		return false
-	}
-
+func (c Config) IsSink(path, recv, name string) bool {
 	for _, p := range c.Sinks {
-		if p.Match(f) {
+		if p.MatchFunction(path, recv, name) {
 			return true
 		}
 	}
 	return false
 }
 
-func (c Config) IsSanitizer(call *ssa.Call) bool {
-	callee := call.Call.StaticCallee()
-	if callee == nil {
-		return false
-	}
-
+func (c Config) IsSanitizer(path, recv, name string) bool {
 	for _, p := range c.Sanitizers {
-		if p.Match(callee) {
+		if p.MatchFunction(path, recv, name) {
 			return true
 		}
 	}
 	return false
 }
 
-func (c Config) IsSource(t types.Type) bool {
-	n, ok := t.(*types.Named)
-	if !ok {
-		return false
-	}
-
+func (c Config) IsSourceType(path, name string) bool {
 	for _, p := range c.Sources {
-		if p.match(n) {
+		if p.MatchType(path, name) {
 			return true
 		}
 	}
 	return false
 }
 
-func (c Config) IsSourceField(typ types.Type, fld *types.Var) bool {
-	n, ok := typ.(*types.Named)
-	if !ok {
-		return false
-	}
-
+func (c Config) IsSourceField(path, typeName, fieldName string) bool {
 	for _, p := range c.Sources {
-		if p.match(n) && p.FieldRE.MatchString(fld.Name()) {
-			return true
-		}
-	}
-	return false
-}
-
-func (c Config) IsSourceFieldAddr(fa *ssa.FieldAddr) bool {
-	// fa.Type() refers to the accessed field's type.
-	// fa.X.Type() refers to the surrounding struct's type.
-	deref := utils.Dereference(fa.X.Type())
-	fieldName := utils.FieldName(fa)
-
-	n, ok := deref.(*types.Named)
-	if !ok {
-		return false
-	}
-
-	for _, p := range c.Sources {
-		if p.match(n) && p.FieldRE.MatchString(fieldName) {
+		if p.MatchField(path, typeName, fieldName) {
 			return true
 		}
 	}
@@ -180,13 +129,12 @@ type sourceMatcher struct {
 	FieldRE   regexp.Regexp
 }
 
-func (s sourceMatcher) match(n *types.Named) bool {
-	if types.IsInterface(n) {
-		// In our context, both sources and sanitizers are concrete types.
-		return false
-	}
+func (s sourceMatcher) MatchType(path, typeName string) bool {
+	return s.PackageRE.MatchString(path) && s.TypeRE.MatchString(typeName)
+}
 
-	return s.PackageRE.MatchString(n.Obj().Pkg().Path()) && s.TypeRE.MatchString(n.Obj().Name())
+func (s sourceMatcher) MatchField(path, typeName, fieldName string) bool {
+	return s.PackageRE.MatchString(path) && s.TypeRE.MatchString(typeName) && s.FieldRE.MatchString(fieldName)
 }
 
 type funcMatcher struct {
@@ -195,33 +143,8 @@ type funcMatcher struct {
 	MethodRE   regexp.Regexp
 }
 
-func (fm funcMatcher) matchPackage(p *types.Package) bool {
-	return fm.PackageRE.MatchString(p.Path())
-}
-
-// Match matches methods based on package, method, and receiver regexp.
-// To explicitly match a method with no receiver (i.e., a top-level function),
-// provide the ReceiverRE regexp `^$`.
-func (fm funcMatcher) Match(f *ssa.Function) bool {
-	if !fm.matchPackage(f.Pkg.Pkg) || !fm.MethodRE.MatchString(f.Name()) {
-		return false
-	}
-
-	recv := f.Signature.Recv()
-	var recvName string
-	if recv != nil {
-		recvName = unqualifiedName(recv)
-	}
-	return fm.ReceiverRE.MatchString(recvName)
-}
-
-func unqualifiedName(v *types.Var) string {
-	packageQualifiedName := v.Type().String()
-	dotPos := strings.LastIndexByte(packageQualifiedName, '.')
-	if dotPos == -1 {
-		return packageQualifiedName
-	}
-	return packageQualifiedName[dotPos+1:]
+func (fm funcMatcher) MatchFunction(path, receiver, name string) bool {
+	return fm.PackageRE.MatchString(path) && fm.ReceiverRE.MatchString(receiver) && fm.MethodRE.MatchString(name)
 }
 
 var readFileOnce sync.Once
diff --git a/internal/pkg/fieldpropagator/analyzer.go b/internal/pkg/fieldpropagator/analyzer.go
index 6812283..8904cb8 100644
--- a/internal/pkg/fieldpropagator/analyzer.go
+++ b/internal/pkg/fieldpropagator/analyzer.go
@@ -22,6 +22,7 @@ import (
 
 	"github.com/google/go-flow-levee/internal/pkg/config"
 	"github.com/google/go-flow-levee/internal/pkg/fieldtags"
+	"github.com/google/go-flow-levee/internal/pkg/utils"
 	"golang.org/x/tools/go/analysis"
 	"golang.org/x/tools/go/analysis/passes/buildssa"
 	"golang.org/x/tools/go/ssa"
@@ -68,7 +69,7 @@ func run(pass *analysis.Pass) (interface{}, error) {
 	ssaProg := ssaInput.Pkg.Prog
 	for _, mem := range ssaInput.Pkg.Members {
 		ssaType, ok := mem.(*ssa.Type)
-		if !ok || !conf.IsSource(ssaType.Type()) {
+		if !ok || !conf.IsSourceType(utils.DecomposeType(ssaType.Type())) {
 			continue
 		}
 		methods := ssaProg.MethodSets.MethodSet(ssaType.Type())
@@ -109,7 +110,11 @@ func analyzeResults(pass *analysis.Pass, conf *config.Config, tf fieldtags.Resul
 		if !ok {
 			continue
 		}
-		if conf.IsSourceFieldAddr(fa) || tf.IsSourceFieldAddr(fa) {
+
+		deref := utils.Dereference(fa.X.Type())
+		path, typeName := utils.DecomposeType(deref)
+		fieldName := utils.FieldName(fa)
+		if conf.IsSourceField(path, typeName, fieldName) || tf.IsSourceFieldAddr(fa) {
 			pass.ExportObjectFact(meth.Object(), &isFieldPropagator{})
 		}
 	}
diff --git a/internal/pkg/levee/levee.go b/internal/pkg/levee/levee.go
index edf1903..98f8b22 100644
--- a/internal/pkg/levee/levee.go
+++ b/internal/pkg/levee/levee.go
@@ -20,6 +20,7 @@ import (
 
 	"github.com/google/go-flow-levee/internal/pkg/config"
 	"github.com/google/go-flow-levee/internal/pkg/fieldpropagator"
+	"github.com/google/go-flow-levee/internal/pkg/utils"
 	"golang.org/x/tools/go/analysis"
 	"golang.org/x/tools/go/ssa"
 
@@ -54,11 +55,13 @@ func run(pass *analysis.Pass) (interface{}, error) {
 				if !ok {
 					continue
 				}
+
+				callee := v.Call.StaticCallee()
 				switch {
 				case fieldPropagators.IsFieldPropagator(v):
 					sources = append(sources, source.New(v, conf))
 
-				case conf.IsSinkCall(v):
+				case callee != nil && conf.IsSink(utils.DecomposeFunction(v.Call.StaticCallee())):
 					for _, s := range sources {
 						if s.HasPathTo(instr.(ssa.Node)) && !s.IsSanitizedAt(v) {
 							report(pass, s, v)
diff --git a/internal/pkg/source/source.go b/internal/pkg/source/source.go
index e7ac0f7..fc4731c 100644
--- a/internal/pkg/source/source.go
+++ b/internal/pkg/source/source.go
@@ -29,11 +29,11 @@ import (
 )
 
 type classifier interface {
-	IsSource(types.Type) bool
-	IsSanitizer(*ssa.Call) bool
-	IsSourceFieldAddr(*ssa.FieldAddr) bool
-	IsSinkFunction(fn *ssa.Function) bool
-	IsExcluded(fn *ssa.Function) bool
+	IsSourceType(path, typeName string) bool
+	IsSourceField(path, typeName, fieldName string) bool
+	IsSanitizer(path, recv, name string) bool
+	IsSink(path, recv, name string) bool
+	IsExcluded(path, recv, name string) bool
 }
 
 // Source represents a Source in an SSA call tree.
@@ -125,7 +125,7 @@ func (s *Source) visitReferrers(n ssa.Node, maxInstrReached map[*ssa.BasicBlock]
 	for _, r := range referrers {
 		switch v := r.(type) {
 		case *ssa.Call:
-			if s.config.IsSanitizer(v) {
+			if callee := v.Call.StaticCallee(); callee != nil && s.config.IsSanitizer(utils.DecomposeFunction(callee)) {
 				s.sanitizers = append(s.sanitizers, &sanitizer.Sanitizer{Call: v})
 			}
 		}
@@ -145,7 +145,7 @@ func (s *Source) referrersToVisit(n ssa.Node, maxInstrReached map[*ssa.BasicBloc
 		if c, ok := r.(*ssa.Call); ok {
 			// This is to avoid attaching calls where the source is the receiver, ex:
 			// core.Sinkf("Source id: %v", wrapper.Source.GetID())
-			if recv := c.Call.Signature().Recv(); recv != nil && s.config.IsSource(utils.Dereference(recv.Type())) {
+			if recv := c.Call.Signature().Recv(); recv != nil && s.config.IsSourceType(utils.DecomposeType(utils.Dereference(recv.Type()))) {
 				continue
 			}
 
@@ -160,8 +160,14 @@ func (s *Source) referrersToVisit(n ssa.Node, maxInstrReached map[*ssa.BasicBloc
 			}
 		}
 
-		if fa, ok := r.(*ssa.FieldAddr); ok && !s.config.IsSourceFieldAddr(fa) {
-			continue
+		if fa, ok := r.(*ssa.FieldAddr); ok {
+			deref := utils.Dereference(fa.X.Type())
+			typPath, typName := utils.DecomposeType(deref)
+			fieldName := utils.FieldName(fa)
+
+			if !s.config.IsSourceField(typPath, typName, fieldName) {
+				continue
+			}
 		}
 
 		referrers = append(referrers, r)
@@ -284,7 +290,8 @@ func identify(conf classifier, ssaInput *buildssa.SSA) map[*ssa.Function][]*Sour
 
 	for _, fn := range ssaInput.SrcFuncs {
 		// no need to analyze the body of sinks, nor of excluded functions
-		if conf.IsSinkFunction(fn) || conf.IsExcluded(fn) {
+		path, recv, name := utils.DecomposeFunction(fn)
+		if conf.IsSink(path, recv, name) || conf.IsExcluded(path, recv, name) {
 			continue
 		}
 
@@ -317,7 +324,7 @@ func sourcesFromClosure(fn *ssa.Function, conf classifier) []*Source {
 		case *types.Pointer:
 			// FreeVars (variables from a closure) appear as double-pointers
 			// Hence, the need to dereference them recursively.
-			if s, ok := utils.Dereference(t).(*types.Named); ok && conf.IsSource(s) {
+			if s, ok := utils.Dereference(t).(*types.Named); ok && conf.IsSourceType(utils.DecomposeType(s)) {
 				sources = append(sources, New(p, conf))
 			}
 		}
@@ -354,7 +361,7 @@ func sourcesFromBlocks(fn *ssa.Function, conf classifier) []*Source {
 			// have an Alloc and we'll miss it.
 			case *ssa.Extract:
 				t := v.Tuple.Type().(*types.Tuple).At(v.Index).Type()
-				if _, ok := t.(*types.Pointer); ok && conf.IsSource(utils.Dereference(t)) {
+				if _, ok := t.(*types.Pointer); ok && conf.IsSourceType(utils.DecomposeType(utils.Dereference(t))) {
 					sources = append(sources, New(v, conf))
 				}
 				continue
@@ -395,7 +402,7 @@ func isSourceType(c classifier, t types.Type) bool {
 	deref := utils.Dereference(t)
 	switch tt := deref.(type) {
 	case *types.Named:
-		return c.IsSource(tt) || isSourceType(c, tt.Underlying())
+		return c.IsSourceType(utils.DecomposeType(tt)) || isSourceType(c, tt.Underlying())
 	case *types.Array:
 		return isSourceType(c, tt.Elem())
 	case *types.Slice:
@@ -429,7 +436,7 @@ func isProducedBySanitizer(v ssa.Value, conf classifier) bool {
 		if !ok {
 			continue
 		}
-		if conf.IsSanitizer(call) {
+		if callee := call.Call.StaticCallee(); callee != nil && conf.IsSanitizer(utils.DecomposeFunction(callee)) {
 			return true
 		}
 	}
diff --git a/internal/pkg/sourceinfer/analyzer.go b/internal/pkg/sourceinfer/analyzer.go
index de89715..4f3e836 100644
--- a/internal/pkg/sourceinfer/analyzer.go
+++ b/internal/pkg/sourceinfer/analyzer.go
@@ -274,7 +274,7 @@ func topoSort(graph objectGraph) []types.Object {
 
 func isSourceType(c *config.Config, t types.Type) bool {
 	for nt := range findNamedTypes(t) {
-		if c.IsSource(nt) {
+		if c.IsSourceType(utils.DecomposeType(nt)) {
 			return true
 		}
 	}
diff --git a/internal/pkg/sourcetype/analyzer.go b/internal/pkg/sourcetype/analyzer.go
index 74aa9a3..4d9afb6 100644
--- a/internal/pkg/sourcetype/analyzer.go
+++ b/internal/pkg/sourcetype/analyzer.go
@@ -25,6 +25,7 @@ import (
 
 	"github.com/google/go-flow-levee/internal/pkg/config"
 	"github.com/google/go-flow-levee/internal/pkg/fieldtags"
+	"github.com/google/go-flow-levee/internal/pkg/utils"
 	"golang.org/x/tools/go/analysis"
 	"golang.org/x/tools/go/analysis/passes/buildssa"
 	"golang.org/x/tools/go/ssa"
@@ -97,7 +98,7 @@ func run(pass *analysis.Pass) (interface{}, error) {
 	// Members contains all named entities
 	for _, mem := range ssaInput.Pkg.Members {
 		if ssaType, ok := mem.(*ssa.Type); ok &&
-			(conf.IsSource(ssaType.Type()) || hasTaggedField(mem, taggedFields)) {
+			(conf.IsSourceType(utils.DecomposeType(ssaType.Type())) || hasTaggedField(mem, taggedFields)) {
 			exportSourceFacts(pass, ssaType, conf, taggedFields)
 		}
 	}
@@ -135,7 +136,9 @@ func exportSourceFacts(pass *analysis.Pass, ssaType *ssa.Type, conf *config.Conf
 			if fld.Pkg() != pass.Pkg {
 				continue
 			}
-			if conf.IsSourceField(ssaType.Type(), fld) || taggedFields.IsSource(fld) {
+
+			typPath, typName := utils.DecomposeType(ssaType.Type())
+			if conf.IsSourceField(typPath, typName, fld.Name()) || taggedFields.IsSource(fld) {
 				pass.ExportObjectFact(fld, &fieldDeclFact{})
 			}
 		}
diff --git a/internal/pkg/utils/utils.go b/internal/pkg/utils/utils.go
index 768430d..955b29a 100644
--- a/internal/pkg/utils/utils.go
+++ b/internal/pkg/utils/utils.go
@@ -17,6 +17,7 @@ package utils
 
 import (
 	"go/types"
+	"strings"
 
 	"golang.org/x/tools/go/ssa"
 )
@@ -45,3 +46,42 @@ func FieldName(fa *ssa.FieldAddr) string {
 	}
 	return st.Field(fa.Field).Name()
 }
+
+// DecomposeType returns the path and name of a Named type
+// Returns empty strings if the type is not *types.Named
+func DecomposeType(t types.Type) (path, name string) {
+	n, ok := t.(*types.Named)
+	if !ok {
+		return
+	}
+
+	if pkg := n.Obj().Pkg(); pkg != nil {
+		path = pkg.Path()
+	}
+
+	return path, n.Obj().Name()
+}
+
+func unqualifiedName(v *types.Var) string {
+	packageQualifiedName := v.Type().String()
+	dotPos := strings.LastIndexByte(packageQualifiedName, '.')
+	if dotPos == -1 {
+		return packageQualifiedName
+	}
+	return packageQualifiedName[dotPos+1:]
+}
+
+// DecomposeFunction returns the path, receiver, and name strings of a ssa.Function.
+// For functions that have no receiver, returns an empty string for recv.
+// For shared functions (wrappers and error.Error), returns an empty string for path.
+// Panics if provided a nil argument.
+func DecomposeFunction(f *ssa.Function) (path, recv, name string) {
+	if f.Pkg != nil {
+		path = f.Pkg.Pkg.Path()
+	}
+	name = f.Name()
+	if recvVar := f.Signature.Recv(); recvVar != nil {
+		recv = unqualifiedName(recvVar)
+	}
+	return
+}


Test patch:
diff --git a/internal/pkg/config/config_test.go b/internal/pkg/config/config_test.go
index 05b43c0..cd7dee9 100644
--- a/internal/pkg/config/config_test.go
+++ b/internal/pkg/config/config_test.go
@@ -18,6 +18,7 @@ import (
 	"path/filepath"
 	"testing"
 
+	"github.com/google/go-flow-levee/internal/pkg/utils"
 	"golang.org/x/tools/go/analysis"
 	"golang.org/x/tools/go/analysis/analysistest"
 	"golang.org/x/tools/go/analysis/passes/buildssa"
@@ -40,16 +41,18 @@ func runTest(pass *analysis.Pass) (interface{}, error) {
 	}
 
 	for _, f := range in.SrcFuncs {
-		if conf.IsSinkFunction(f) {
+		if conf.IsSink(utils.DecomposeFunction(f)) {
 			pass.Reportf(f.Pos(), "sink")
 		}
-		if conf.IsExcluded(f) {
+		if conf.IsExcluded(utils.DecomposeFunction(f)) {
 			pass.Reportf(f.Pos(), "excluded")
 		}
 		for _, b := range f.Blocks {
 			for _, i := range b.Instrs {
-				if c, ok := i.(*ssa.Call); ok && conf.IsSinkCall(c) {
-					pass.Reportf(i.Pos(), "sink call")
+				if c, ok := i.(*ssa.Call); ok {
+					if callee := c.Call.StaticCallee(); callee != nil && conf.IsSink(utils.DecomposeFunction(callee)) {
+						pass.Reportf(i.Pos(), "sink call")
+					}
 				}
 			}
 		}
@@ -63,10 +66,5 @@ func TestConfig(t *testing.T) {
 	if err := FlagSet.Set("config", filepath.Join(testdata, "test-config.yaml")); err != nil {
 		t.Fatal(err)
 	}
-	for _, p := range []string{"core", "crosspkg", "exclusion", "notcore"} {
-		analysistest.Run(t, testdata, testAnalyzer, filepath.Join(testdata, "src/example.com", p))
-	}
-	for _, p := range []string{"core", "exclusion"} {
-		analysistest.Run(t, testdata, testAnalyzer, filepath.Join(testdata, "src/notexample.com", p))
-	}
+	analysistest.Run(t, testdata, testAnalyzer, "./...")
 }
diff --git a/internal/pkg/fieldpropagator/analyzer_test.go b/internal/pkg/fieldpropagator/analyzer_test.go
index 23e86f1..ef45712 100644
--- a/internal/pkg/fieldpropagator/analyzer_test.go
+++ b/internal/pkg/fieldpropagator/analyzer_test.go
@@ -27,5 +27,5 @@ func TestFieldPropagatorAnalysis(t *testing.T) {
 	if err := config.FlagSet.Set("config", filepath.Join(testdata, "test-config.yaml")); err != nil {
 		t.Error(err)
 	}
-	analysistest.Run(t, testdata, Analyzer, "source", "test")
+	analysistest.Run(t, testdata, Analyzer, "./...")
 }
diff --git a/internal/pkg/fieldtags/analyzer_test.go b/internal/pkg/fieldtags/analyzer_test.go
index b1a58f3..262ac5e 100644
--- a/internal/pkg/fieldtags/analyzer_test.go
+++ b/internal/pkg/fieldtags/analyzer_test.go
@@ -31,7 +31,7 @@ func TestFieldTagsAnalysis(t *testing.T) {
 		t.Error(err)
 	}
 
-	results := analysistest.Run(t, testdata, Analyzer, "tests")
+	results := analysistest.Run(t, testdata, Analyzer, "./...")
 
 	if len(results) != 1 {
 		t.Fatalf("expected 1 result, got %d", len(results))
diff --git a/internal/pkg/levee/levee_test.go b/internal/pkg/levee/levee_test.go
index 486fb02..1c53106 100644
--- a/internal/pkg/levee/levee_test.go
+++ b/internal/pkg/levee/levee_test.go
@@ -16,10 +16,6 @@ package internal
 
 import (
 	"flag"
-	"fmt"
-	"io/ioutil"
-	"path/filepath"
-	"strings"
 	"testing"
 
 	"github.com/google/go-flow-levee/internal/pkg/debug"
@@ -33,43 +29,8 @@ func TestLevee(t *testing.T) {
 	if err := Analyzer.Flags.Set("config", dataDir+"/test-config.yaml"); err != nil {
 		t.Error(err)
 	}
-	testsDir := filepath.Join(dataDir, "src/example.com/tests")
-	patterns := findTestPatterns(t, testsDir)
 	if *debugging {
 		Analyzer.Requires = append(Analyzer.Requires, debug.Analyzer)
 	}
-	analysistest.Run(t, dataDir, Analyzer, patterns...)
-}
-
-func findTestPatterns(t *testing.T, testsDir string) (patterns []string) {
-	t.Helper()
-	files, err := ioutil.ReadDir(testsDir)
-	if err != nil {
-		t.Fatalf("Failed to read tests dir (%s): %v", testsDir, err)
-	}
-	for _, f := range files {
-		path := filepath.Join(testsDir, f.Name())
-		// Make sure the directory contains a go file to avoid failure on empty directories.
-		if err := checkForGoFiles(path); err != nil {
-			t.Fatalf("Could not verify presence of Go files in test directory: %v", err)
-		}
-
-		patterns = append(patterns, path)
-	}
-	return
-}
-
-func checkForGoFiles(path string) error {
-	pkgFiles, err := ioutil.ReadDir(path)
-	if err != nil {
-		return fmt.Errorf("failed to examine test directory (%s): %v", path, err)
-	}
-
-	for _, file := range pkgFiles {
-		if strings.HasSuffix(file.Name(), ".go") {
-			return nil
-		}
-	}
-	return fmt.Errorf("found no Go files in test directory (%s)", path)
-
+	analysistest.Run(t, dataDir, Analyzer, "./...")
 }
diff --git a/internal/pkg/source/source_test.go b/internal/pkg/source/source_test.go
index 0459e70..e7af9c4 100644
--- a/internal/pkg/source/source_test.go
+++ b/internal/pkg/source/source_test.go
@@ -15,16 +15,12 @@
 package source
 
 import (
-	"go/types"
 	"regexp"
 	"testing"
 
-	"github.com/google/go-flow-levee/internal/pkg/utils"
-
 	"golang.org/x/tools/go/analysis"
 	"golang.org/x/tools/go/analysis/analysistest"
 	"golang.org/x/tools/go/analysis/passes/buildssa"
-	"golang.org/x/tools/go/ssa"
 )
 
 type testConfig struct {
@@ -34,33 +30,27 @@ type testConfig struct {
 	sinkPattern      string
 }
 
-func (c *testConfig) IsSource(t types.Type) bool {
-	d := utils.Dereference(t)
-	_, ok := d.(*types.Named)
-	if !ok {
-		return false
-	}
-
-	match, _ := regexp.MatchString(c.sourcePattern, d.String())
+func (c *testConfig) IsSourceType(path, typename string) bool {
+	match, _ := regexp.MatchString(c.sourcePattern, typename)
 	return match
 }
 
-func (c *testConfig) IsSanitizer(call *ssa.Call) bool {
-	match, _ := regexp.MatchString(c.sanitizerPattern, call.String())
+func (c *testConfig) IsSanitizer(path, recv, name string) bool {
+	match, _ := regexp.MatchString(c.sanitizerPattern, name)
 	return match
 }
 
-func (c *testConfig) IsSourceFieldAddr(field *ssa.FieldAddr) bool {
-	match, _ := regexp.MatchString(c.fieldsPattern, utils.FieldName(field))
+func (c *testConfig) IsSourceField(path, typeName, fieldName string) bool {
+	match, _ := regexp.MatchString(c.fieldsPattern, fieldName)
 	return match
 }
 
-func (c *testConfig) IsSinkFunction(f *ssa.Function) bool {
-	match, _ := regexp.MatchString(c.sinkPattern, f.Name())
+func (c *testConfig) IsSink(path, recv, name string) bool {
+	match, _ := regexp.MatchString(c.sinkPattern, name)
 	return match
 }
 
-func (c *testConfig) IsExcluded(f *ssa.Function) bool {
+func (c *testConfig) IsExcluded(path, recv, name string) bool {
 	return false
 }
 
@@ -74,7 +64,7 @@ var testAnalyzer = &analysis.Analyzer{
 func runTest(pass *analysis.Pass) (interface{}, error) {
 	in := pass.ResultOf[buildssa.Analyzer].(*buildssa.SSA)
 	config := &testConfig{
-		sourcePattern:    `\.foo`,
+		sourcePattern:    "foo",
 		sanitizerPattern: "sanitizer",
 		fieldsPattern:    "name",
 		sinkPattern:      "sink",
diff --git a/internal/pkg/sourceinfer/analyzer_test.go b/internal/pkg/sourceinfer/analyzer_test.go
index abb6be1..21ab38a 100644
--- a/internal/pkg/sourceinfer/analyzer_test.go
+++ b/internal/pkg/sourceinfer/analyzer_test.go
@@ -29,7 +29,5 @@ func TestInferAnalysis(t *testing.T) {
 		t.Error(err)
 	}
 
-	for _, testPkg := range []string{"core", "crosspkg", "samepkg", "nosource"} {
-		analysistest.Run(t, testdata, Analyzer, filepath.Join(testdata, "src/example.com/tests", testPkg))
-	}
+	analysistest.Run(t, testdata, Analyzer, "./...")
 }
diff --git a/internal/pkg/sourceinfer/testdata/src/example.com/tests/simple/simple.go b/internal/pkg/sourceinfer/testdata/src/example.com/tests/simple/simple.go
deleted file mode 100644
index b6a0afc..0000000
--- a/internal/pkg/sourceinfer/testdata/src/example.com/tests/simple/simple.go
+++ /dev/null
@@ -1,23 +0,0 @@
-// Copyright 2020 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// https://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package simple
-
-import (
-	"example.com/source"
-)
-
-type SourceHolder struct { // want SourceHolder:"inferred source"
-	s source.Source
-}
diff --git a/internal/pkg/sourcetype/analyzer_test.go b/internal/pkg/sourcetype/analyzer_test.go
index 9951266..6a69e2f 100644
--- a/internal/pkg/sourcetype/analyzer_test.go
+++ b/internal/pkg/sourcetype/analyzer_test.go
@@ -28,5 +28,5 @@ func TestSourceAnalysis(t *testing.T) {
 		return
 	}
 
-	analysistest.Run(t, testdata, Analyzer, "sourcetype", "crosspkg")
+	analysistest.Run(t, testdata, Analyzer, "./...")
 }