Repository: google/go-flow-levee (go)

Issue:
Use propagation logic from the main analyzer in the fieldpropagator analyzer
Currently, the analysis performed by the `fieldpropagator` analyzer to determine if a given method on a `Source` type is a `Field propagator` is very basic, and it only works for simple getters. As just one example, the following method is not identified as a `Field propagator`:

```go
func (s Source) DerivedFromData() string { // want DerivedFromData:"field propagator identified"
	derived := fmt.Sprintf("%s", s.data)
	return derived
}
```

The main analyzer would identify `derived` as being tainted by `s.data`, but the `fieldpropagator` analyzer does not. Currently, we do not propagate taint to the return value of a method on a `Source` type unless the method is a `Field propagator`. This means that we would not find a potential leak if it involves a `Field propagator` like the one above.

I think both analyzers should use the same propagation logic.

(This will also apply to interprocedural analysis when we get there.)

Pull request:
Use propagation in fieldpropagator
Fixes #130.

This PR is a chain of 3 commits, with the last one being the desired change, and the first 2 being refactorings necessary to get there. Reviewers may wish to consult each commit separately.

The first commit, `move propagation to its own package`, extracts the `propagation` package from `levee` into its own package. This makes clear the fact that this logic has nothing levee-specific and that it can be used in other analyzers.

The second commit, `move sourcetype identification logic to sourcetype package`, moves the `IsSourceType` function from the `source` package to the `sourcetype` package in order to break an import cycle. Indeed, the dependencies are: `propagation --> source --> fieldpropagator`, so `fieldpropagator` cannot depend on `propagation` without creating a cycle. The only reason that `propagation` depends on `source` is because it uses the `IsSourceType` function. This function belongs in the `sourcetype` package, and extracting it removes the cycle. Since the `sourcetype` analyzer isn't used and much of its logic is redundant with that function, I removed the existing files in the `sourcetype` package.

The final commit, `use propagation in fieldpropagator`, enables the `fieldpropagator` analyzer to use the propagation logic from the `propagation`  package, instead of the much simpler logic it was using. Tests are added to verify this behavior.

- [x] Running against a large codebase such as [Kubernetes](https://github.com/kubernetes/kubernetes) does not error out. (See [DEVELOPING.md](https://github.com/google/go-flow-levee/blob/master/DEVELOPING.md) for instructions on how to do that.)
- (N/A) [ ] Appropriate changes to README are included in PR


Patch:
diff --git a/cmd/sourcetype/main.go b/cmd/sourcetype/main.go
deleted file mode 100644
index 2a41e54..0000000
--- a/cmd/sourcetype/main.go
+++ /dev/null
@@ -1,26 +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 main
-
-import (
-	"github.com/google/go-flow-levee/internal/pkg/sourcetype"
-	"golang.org/x/tools/go/analysis/singlechecker"
-)
-
-func main() {
-	sourcetype.Report = true
-
-	singlechecker.Main(sourcetype.Analyzer)
-}
diff --git a/internal/pkg/fieldpropagator/analyzer.go b/internal/pkg/fieldpropagator/analyzer.go
index 4d9fc0e..1883f62 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/propagation"
 	"github.com/google/go-flow-levee/internal/pkg/utils"
 	"golang.org/x/tools/go/analysis"
 	"golang.org/x/tools/go/analysis/passes/buildssa"
@@ -49,7 +50,7 @@ var Analyzer = &analysis.Analyzer{
 	Name: "fieldpropagator",
 	Doc: `This analyzer identifies field propagators.
 
-A field propagator is a function that returns a source field.`,
+A field propagator is a function that returns a value that is tainted by a source field.`,
 	Flags:      config.FlagSet,
 	Run:        run,
 	Requires:   []*analysis.Analyzer{buildssa.Analyzer, fieldtags.Analyzer},
@@ -105,43 +106,41 @@ func methodValues(ssaProg *ssa.Program, t types.Type) []*ssa.Function {
 }
 
 func analyzeBlocks(pass *analysis.Pass, conf *config.Config, tf fieldtags.ResultType, meth *ssa.Function) {
-	// Function does not return anything
-	if res := meth.Signature.Results(); res == nil || (*res).Len() == 0 {
-		return
-	}
+	var propagations []propagation.Propagation
+
 	for _, b := range meth.Blocks {
-		if len(b.Instrs) == 0 {
-			continue
+		for _, instr := range b.Instrs {
+			var (
+				txType types.Type
+				field  int
+			)
+			switch t := instr.(type) {
+			case *ssa.Field:
+				txType = t.X.Type()
+				field = t.Field
+			case *ssa.FieldAddr:
+				txType = t.X.Type()
+				field = t.Field
+			default:
+				continue
+			}
+			if conf.IsSourceField(utils.DecomposeField(txType, field)) || tf.IsSourceField(txType, field) {
+				propagations = append(propagations, propagation.Dfs(instr.(ssa.Node), conf, tf))
+			}
 		}
-		lastInstr := b.Instrs[len(b.Instrs)-1]
-		ret, ok := lastInstr.(*ssa.Return)
-		if !ok {
-			continue
-		}
-		analyzeResults(pass, conf, tf, meth, ret.Results)
 	}
-}
 
-func analyzeResults(pass *analysis.Pass, conf *config.Config, tf fieldtags.ResultType, meth *ssa.Function, results []ssa.Value) {
-	for _, r := range results {
-		fa, ok := fieldAddr(r)
-		if !ok {
-			continue
-		}
-
-		xt, field := fa.X.Type(), fa.Field
-		if conf.IsSourceField(utils.DecomposeField(xt, field)) || tf.IsSourceField(xt, field) {
-			pass.ExportObjectFact(meth.Object(), &isFieldPropagator{})
+	for _, b := range meth.Blocks {
+		for _, instr := range b.Instrs {
+			ret, ok := instr.(*ssa.Return)
+			if !ok {
+				continue
+			}
+			for _, prop := range propagations {
+				if prop.IsTainted(ret) {
+					pass.ExportObjectFact(meth.Object(), &isFieldPropagator{})
+				}
+			}
 		}
 	}
 }
-
-func fieldAddr(x ssa.Value) (*ssa.FieldAddr, bool) {
-	switch t := x.(type) {
-	case *ssa.FieldAddr:
-		return t, true
-	case *ssa.UnOp:
-		return fieldAddr(t.X)
-	}
-	return nil, false
-}
diff --git a/internal/pkg/levee/levee.go b/internal/pkg/levee/levee.go
index ec7649c..d244c56 100644
--- a/internal/pkg/levee/levee.go
+++ b/internal/pkg/levee/levee.go
@@ -20,7 +20,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/levee/propagation"
+	"github.com/google/go-flow-levee/internal/pkg/propagation"
 	"github.com/google/go-flow-levee/internal/pkg/utils"
 	"golang.org/x/tools/go/analysis"
 	"golang.org/x/tools/go/ssa"
diff --git a/internal/pkg/levee/propagation/propagation.go b/internal/pkg/propagation/propagation.go
similarity index 98%
rename from internal/pkg/levee/propagation/propagation.go
rename to internal/pkg/propagation/propagation.go
index 53d1fc8..10fcdd4 100644
--- a/internal/pkg/levee/propagation/propagation.go
+++ b/internal/pkg/propagation/propagation.go
@@ -24,7 +24,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/sanitizer"
-	"github.com/google/go-flow-levee/internal/pkg/source"
+	"github.com/google/go-flow-levee/internal/pkg/sourcetype"
 	"github.com/google/go-flow-levee/internal/pkg/utils"
 	"golang.org/x/tools/go/pointer"
 	"golang.org/x/tools/go/ssa"
@@ -156,7 +156,7 @@ func (prop *Propagation) visit(n ssa.Node, maxInstrReached map[*ssa.BasicBlock]i
 
 		// This is to avoid attaching calls where the source is the receiver, ex:
 		// core.Sinkf("Source id: %v", wrapper.Source.GetID())
-		if recv := t.Call.Signature().Recv(); recv != nil && source.IsSourceType(prop.config, prop.taggedFields, recv.Type()) {
+		if recv := t.Call.Signature().Recv(); recv != nil && sourcetype.IsSourceType(prop.config, prop.taggedFields, recv.Type()) {
 			return
 		}
 
diff --git a/internal/pkg/source/source.go b/internal/pkg/source/source.go
index 176efe8..8cced10 100644
--- a/internal/pkg/source/source.go
+++ b/internal/pkg/source/source.go
@@ -16,13 +16,13 @@
 package source
 
 import (
-	"fmt"
 	"go/token"
 	"go/types"
 
 	"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/fieldtags"
+	"github.com/google/go-flow-levee/internal/pkg/sourcetype"
 	"github.com/google/go-flow-levee/internal/pkg/utils"
 	"golang.org/x/tools/go/analysis/passes/buildssa"
 	"golang.org/x/tools/go/ssa"
@@ -90,7 +90,7 @@ func identify(conf *config.Config, ssaInput *buildssa.SSA, taggedFields fieldtag
 func sourcesFromParams(fn *ssa.Function, conf *config.Config, taggedFields fieldtags.ResultType) []*Source {
 	var sources []*Source
 	for _, p := range fn.Params {
-		if IsSourceType(conf, taggedFields, p.Type()) {
+		if sourcetype.IsSourceType(conf, taggedFields, p.Type()) {
 			sources = append(sources, New(p))
 		}
 	}
@@ -106,7 +106,7 @@ func sourcesFromClosures(fn *ssa.Function, conf *config.Config, taggedFields fie
 	for _, p := range fn.FreeVars {
 		switch t := p.Type().(type) {
 		case *types.Pointer:
-			if IsSourceType(conf, taggedFields, t) {
+			if sourcetype.IsSourceType(conf, taggedFields, t) {
 				sources = append(sources, New(p))
 			}
 		}
@@ -135,19 +135,19 @@ func isSourceNode(n ssa.Node, conf *config.Config, propagators fieldpropagator.R
 
 	// Values produced by sanitizers are not sources.
 	case *ssa.Alloc:
-		return !isProducedBySanitizer(v, conf) && IsSourceType(conf, taggedFields, n.(ssa.Value).Type())
+		return !isProducedBySanitizer(v, conf) && sourcetype.IsSourceType(conf, taggedFields, n.(ssa.Value).Type())
 
 	// Values produced by sanitizers are not sources.
 	// Values produced by field propagators are.
 	case *ssa.Call:
 		return !isProducedBySanitizer(v, conf) &&
-			(propagators.IsFieldPropagator(v) || IsSourceType(conf, taggedFields, n.(ssa.Value).Type()))
+			(propagators.IsFieldPropagator(v) || sourcetype.IsSourceType(conf, taggedFields, n.(ssa.Value).Type()))
 
 	// A type assertion can assert that an interface is of a source type.
 	// Only panicky type asserts will refer to the source Value.
 	// The typed value returned in (value, ok) type assertions are examined in the case for ssa.Extract instructions.
 	case *ssa.TypeAssert:
-		return !v.CommaOk && IsSourceType(conf, taggedFields, v.AssertedType)
+		return !v.CommaOk && sourcetype.IsSourceType(conf, taggedFields, v.AssertedType)
 
 	// An Extract is used to obtain a value from an instruction that returns multiple values.
 	// If the extracted value is a Pointer to a Source, it won't have an Alloc, so we need to
@@ -155,11 +155,11 @@ func isSourceNode(n ssa.Node, conf *config.Config, propagators fieldpropagator.R
 	case *ssa.Extract:
 		t := v.Tuple.Type().(*types.Tuple).At(v.Index).Type()
 		_, ok := t.(*types.Pointer)
-		return ok && IsSourceType(conf, taggedFields, t)
+		return ok && sourcetype.IsSourceType(conf, taggedFields, t)
 
 	// Unary operator <- can receive sources from a channel.
 	case *ssa.UnOp:
-		return v.Op == token.ARROW && IsSourceType(conf, taggedFields, n.(ssa.Value).Type())
+		return v.Op == token.ARROW && sourcetype.IsSourceType(conf, taggedFields, n.(ssa.Value).Type())
 
 	// Field access (Field, FieldAddr),
 	// collection access (Index, IndexAddr, Lookup),
@@ -168,55 +168,10 @@ func isSourceNode(n ssa.Node, conf *config.Config, propagators fieldpropagator.R
 	case *ssa.Field, *ssa.FieldAddr,
 		*ssa.Index, *ssa.IndexAddr, *ssa.Lookup,
 		*ssa.MakeMap, *ssa.MakeChan:
-		return IsSourceType(conf, taggedFields, n.(ssa.Value).Type())
+		return sourcetype.IsSourceType(conf, taggedFields, n.(ssa.Value).Type())
 	}
 }
 
-// IsSourceType determines whether a Type is a Source Type.
-// A Source Type is either:
-// - A Named Type that is classified as a Source
-// - A composite type that contains a Source Type
-// - A Struct Type that contains a tagged field
-func IsSourceType(c *config.Config, tf fieldtags.ResultType, t types.Type) bool {
-	deref := utils.Dereference(t)
-	switch tt := deref.(type) {
-	case *types.Named:
-		return c.IsSourceType(utils.DecomposeType(tt)) || IsSourceType(c, tf, tt.Underlying())
-	case *types.Array:
-		return IsSourceType(c, tf, tt.Elem())
-	case *types.Slice:
-		return IsSourceType(c, tf, tt.Elem())
-	case *types.Chan:
-		return IsSourceType(c, tf, tt.Elem())
-	case *types.Map:
-		key := IsSourceType(c, tf, tt.Key())
-		elem := IsSourceType(c, tf, tt.Elem())
-		return key || elem
-	case *types.Struct:
-		return hasTaggedField(tf, tt)
-	case *types.Basic, *types.Tuple, *types.Interface, *types.Signature:
-		// These types do not currently represent possible source types
-		return false
-	case *types.Pointer:
-		// This should be unreachable due to the dereference above
-		return false
-	default:
-		// The above should be exhaustive.  Reaching this default case is an error.
-		fmt.Printf("unexpected type received: %T %v; please report this issue\n", tt, tt)
-		return false
-	}
-}
-
-func hasTaggedField(taggedFields fieldtags.ResultType, s *types.Struct) bool {
-	for i := 0; i < s.NumFields(); i++ {
-		f := s.Field(i)
-		if taggedFields.IsSource(f) {
-			return true
-		}
-	}
-	return false
-}
-
 func isProducedBySanitizer(v ssa.Value, conf *config.Config) bool {
 	for _, instr := range *v.Referrers() {
 		store, ok := instr.(*ssa.Store)
diff --git a/internal/pkg/sourcetype/analyzer.go b/internal/pkg/sourcetype/analyzer.go
deleted file mode 100644
index 4d9afb6..0000000
--- a/internal/pkg/sourcetype/analyzer.go
+++ /dev/null
@@ -1,164 +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 sourcetpye handles identification of source types and fields at the type declaration.
-// This can be consumed downstream, e.g., by the sources package to identify source data at instantiation.
-// This package concerns itself with ssa.Member and types.Object, as opposed to ssa.Value and ssa.Instruction more typically used in other analysis packages.
-package sourcetype
-
-import (
-	"fmt"
-	"go/types"
-	"reflect"
-	"sort"
-
-	"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"
-)
-
-type typeDeclFact struct{}
-
-func (t typeDeclFact) AFact() {
-}
-
-func (t typeDeclFact) String() string {
-	return "source type"
-}
-
-type fieldDeclFact struct{}
-
-func (f fieldDeclFact) AFact() {
-}
-
-func (f fieldDeclFact) String() string {
-	return "source field"
-}
-
-type sourceClassifier struct {
-	passObjFacts []analysis.ObjectFact
-}
-
-func (s sourceClassifier) IsSource(named *types.Named) bool {
-	if named == nil {
-		return false
-	}
-
-	for _, fct := range s.passObjFacts {
-		if fct.Object == named.Obj() {
-			return true
-		}
-	}
-	return false
-}
-
-func (s sourceClassifier) IsSourceField(v *types.Var) bool {
-	for _, fct := range s.passObjFacts {
-		if fct.Object == v {
-			return true
-		}
-	}
-	return false
-}
-
-var Analyzer = &analysis.Analyzer{
-	Name:       "sourcetypes",
-	Doc:        "This analyzer identifies types.Types values which contain dataflow sources.",
-	Flags:      config.FlagSet,
-	Run:        run,
-	Requires:   []*analysis.Analyzer{buildssa.Analyzer, fieldtags.Analyzer},
-	ResultType: reflect.TypeOf(new(sourceClassifier)),
-	FactTypes:  []analysis.Fact{new(typeDeclFact), new(fieldDeclFact)},
-}
-
-var Report bool
-
-func run(pass *analysis.Pass) (interface{}, error) {
-	taggedFields := pass.ResultOf[fieldtags.Analyzer].(fieldtags.ResultType)
-	ssaInput := pass.ResultOf[buildssa.Analyzer].(*buildssa.SSA)
-	conf, err := config.ReadConfig()
-	if err != nil {
-		return nil, err
-	}
-
-	// Members contains all named entities
-	for _, mem := range ssaInput.Pkg.Members {
-		if ssaType, ok := mem.(*ssa.Type); ok &&
-			(conf.IsSourceType(utils.DecomposeType(ssaType.Type())) || hasTaggedField(mem, taggedFields)) {
-			exportSourceFacts(pass, ssaType, conf, taggedFields)
-		}
-	}
-
-	classifier := &sourceClassifier{pass.AllObjectFacts()}
-	if Report {
-		makeReport(classifier, pass)
-	}
-
-	return classifier, nil
-}
-
-func hasTaggedField(mem ssa.Member, taggedFields fieldtags.ResultType) bool {
-	n, ok := mem.Type().(*types.Named)
-	if !ok {
-		return false
-	}
-	s, ok := n.Underlying().(*types.Struct)
-	if !ok {
-		return false
-	}
-	var has bool
-	for i := 0; i < s.NumFields(); i++ {
-		f := s.Field(i)
-		has = has || taggedFields.IsSource(f)
-	}
-	return has
-}
-
-func exportSourceFacts(pass *analysis.Pass, ssaType *ssa.Type, conf *config.Config, taggedFields fieldtags.ResultType) {
-	pass.ExportObjectFact(ssaType.Object(), &typeDeclFact{})
-	if under, ok := ssaType.Type().Underlying().(*types.Struct); ok {
-		for i := 0; i < under.NumFields(); i++ {
-			fld := under.Field(i)
-			if fld.Pkg() != pass.Pkg {
-				continue
-			}
-
-			typPath, typName := utils.DecomposeType(ssaType.Type())
-			if conf.IsSourceField(typPath, typName, fld.Name()) || taggedFields.IsSource(fld) {
-				pass.ExportObjectFact(fld, &fieldDeclFact{})
-			}
-		}
-	}
-}
-
-func makeReport(classifier *sourceClassifier, pass *analysis.Pass) {
-	// Aggregate diagnostics first in order to sort report by position.
-	var diags []analysis.Diagnostic
-	for _, objFact := range classifier.passObjFacts {
-		// A pass should only report within its package.
-		if objFact.Object.Pkg() == pass.Pkg {
-			diags = append(diags, analysis.Diagnostic{
-				Pos:     objFact.Object.Pos(),
-				Message: fmt.Sprintf("%v: %v", objFact.Fact, objFact.Object.Name()),
-			})
-		}
-	}
-	sort.Slice(diags, func(i, j int) bool { return diags[i].Pos < diags[j].Pos })
-	for _, d := range diags {
-		pass.Reportf(d.Pos, d.Message)
-	}
-}
diff --git a/internal/pkg/sourcetype/sourcetype.go b/internal/pkg/sourcetype/sourcetype.go
new file mode 100644
index 0000000..1e21025
--- /dev/null
+++ b/internal/pkg/sourcetype/sourcetype.go
@@ -0,0 +1,70 @@
+// 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 sourcetype handles identification of sources based on their type.
+package sourcetype
+
+import (
+	"fmt"
+	"go/types"
+
+	"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"
+)
+
+// IsSourceType determines whether a Type is a Source Type.
+// A Source Type is either:
+// - A Named Type that is classified as a Source
+// - A composite type that contains a Source Type
+// - A Struct Type that contains a tagged field
+func IsSourceType(c *config.Config, tf fieldtags.ResultType, t types.Type) bool {
+	deref := utils.Dereference(t)
+	switch tt := deref.(type) {
+	case *types.Named:
+		return c.IsSourceType(utils.DecomposeType(tt)) || IsSourceType(c, tf, tt.Underlying())
+	case *types.Array:
+		return IsSourceType(c, tf, tt.Elem())
+	case *types.Slice:
+		return IsSourceType(c, tf, tt.Elem())
+	case *types.Chan:
+		return IsSourceType(c, tf, tt.Elem())
+	case *types.Map:
+		key := IsSourceType(c, tf, tt.Key())
+		elem := IsSourceType(c, tf, tt.Elem())
+		return key || elem
+	case *types.Struct:
+		return hasTaggedField(tf, tt)
+	case *types.Basic, *types.Tuple, *types.Interface, *types.Signature:
+		// These types do not currently represent possible source types
+		return false
+	case *types.Pointer:
+		// This should be unreachable due to the dereference above
+		return false
+	default:
+		// The above should be exhaustive.  Reaching this default case is an error.
+		fmt.Printf("unexpected type received: %T %v; please report this issue\n", tt, tt)
+		return false
+	}
+}
+
+func hasTaggedField(taggedFields fieldtags.ResultType, s *types.Struct) bool {
+	for i := 0; i < s.NumFields(); i++ {
+		f := s.Field(i)
+		if taggedFields.IsSource(f) {
+			return true
+		}
+	}
+	return false
+}


Test patch:
diff --git a/internal/pkg/fieldpropagator/testdata/src/source/test.go b/internal/pkg/fieldpropagator/testdata/src/source/test.go
index 493e45d..d6a080d 100644
--- a/internal/pkg/fieldpropagator/testdata/src/source/test.go
+++ b/internal/pkg/fieldpropagator/testdata/src/source/test.go
@@ -49,6 +49,10 @@ func (s Source) DataDeref() string { // want DataDeref:"field propagator identif
 	return *s.dataPtr
 }
 
+func (s Source) ShowData() string { // want ShowData:"field propagator identified"
+	return "Data: " + s.data
+}
+
 var isAdmin bool
 
 func (s Source) MaybeData() string { // want MaybeData:"field propagator identified"
diff --git a/internal/pkg/levee/testdata/src/example.com/core/source.go b/internal/pkg/levee/testdata/src/example.com/core/source.go
index ed78011..ba19f85 100644
--- a/internal/pkg/levee/testdata/src/example.com/core/source.go
+++ b/internal/pkg/levee/testdata/src/example.com/core/source.go
@@ -28,6 +28,10 @@ func (s Source) GetData() string {
 	return s.Data
 }
 
+func (s Source) ShowData() string {
+	return "Data: " + s.Data
+}
+
 func (s Source) Copy() (Source, error) {
 	return s, nil
 }
diff --git a/internal/pkg/levee/testdata/src/example.com/tests/fields/tests.go b/internal/pkg/levee/testdata/src/example.com/tests/fields/tests.go
index 5ac8545..94cf574 100644
--- a/internal/pkg/levee/testdata/src/example.com/tests/fields/tests.go
+++ b/internal/pkg/levee/testdata/src/example.com/tests/fields/tests.go
@@ -23,9 +23,11 @@ import (
 
 func TestFieldAccessors(s core.Source, ptr *core.Source) {
 	core.Sinkf("Data: %v", s.GetData()) // want "a source has reached a sink"
+	core.Sinkf(s.ShowData())            // want "a source has reached a sink"
 	core.Sinkf("ID: %v", s.GetID())
 
 	core.Sinkf("Data: %v", ptr.GetData()) // want "a source has reached a sink"
+	core.Sinkf(ptr.ShowData())            // want "a source has reached a sink"
 	core.Sinkf("ID: %v", ptr.GetID())
 }
 
diff --git a/internal/pkg/sourcetype/analyzer_test.go b/internal/pkg/sourcetype/analyzer_test.go
deleted file mode 100644
index 6a69e2f..0000000
--- a/internal/pkg/sourcetype/analyzer_test.go
+++ /dev/null
@@ -1,32 +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 sourcetype
-
-import (
-	"testing"
-
-	"golang.org/x/tools/go/analysis/analysistest"
-)
-
-func TestSourceAnalysis(t *testing.T) {
-	testdata := analysistest.TestData()
-
-	if err := Analyzer.Flags.Set("config", testdata+"/test-config.yaml"); err != nil {
-		t.Error(err)
-		return
-	}
-
-	analysistest.Run(t, testdata, Analyzer, "./...")
-}
diff --git a/internal/pkg/sourcetype/testdata/src/crosspkg/test.go b/internal/pkg/sourcetype/testdata/src/crosspkg/test.go
deleted file mode 100644
index 1a8cc57..0000000
--- a/internal/pkg/sourcetype/testdata/src/crosspkg/test.go
+++ /dev/null
@@ -1,36 +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 crosspkg
-
-import "sourcetype"
-
-type AliasStruct = sourcetype.Source // want AliasStruct:"source type"
-
-type NamedType sourcetype.Source // want NamedType:"source type"
-
-// TODO(#96,#97) Consider automatic detection of the following types.
-
-type SliceContainer []sourcetype.Source
-type ArrayContainer [5]sourcetype.Source
-type MapKeyContainer map[sourcetype.Source]interface{}
-type MapValueContainer map[string]sourcetype.Source
-
-type EmbeddedWrapper struct {
-	sourcetype.Source
-}
-
-type FieldWrapper struct {
-	s sourcetype.Source
-}
diff --git a/internal/pkg/sourcetype/testdata/src/sourcetype/test.go b/internal/pkg/sourcetype/testdata/src/sourcetype/test.go
deleted file mode 100644
index 58e197d..0000000
--- a/internal/pkg/sourcetype/testdata/src/sourcetype/test.go
+++ /dev/null
@@ -1,43 +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 sourcetype
-
-type Source struct { // want Source:"source type"
-	Data         string // want Data:"source field"
-	Tagged       string `levee:"source"` // want Tagged:"source field"
-	Name1, Name2 string `levee:"source"` // want Name1:"source field" Name2:"source field"
-	ID           int
-}
-
-type SourceContainingTaggedField struct { // want SourceContainingTaggedField:"source type"
-	Tagged string `levee:"source"` // want Tagged:"source field"
-}
-
-type AliasStruct = Source // want AliasStruct:"source type"
-
-type NamedType Source // want NamedType:"source type"
-
-type SliceContainer []Source
-type ArrayContainer [5]Source
-type MapKeyContainer map[Source]interface{}
-type MapValueContainer map[string]Source
-
-type EmbeddedWrapper struct {
-	Source
-}
-
-type FieldWrapper struct {
-	s Source
-}
diff --git a/internal/pkg/sourcetype/testdata/test-config.yaml b/internal/pkg/sourcetype/testdata/test-config.yaml
deleted file mode 100644
index 043f7ae..0000000
--- a/internal/pkg/sourcetype/testdata/test-config.yaml
+++ /dev/null
@@ -1,18 +0,0 @@
-# Copyright 2019 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.
----
-Sources:
-- PackageRE: ""
-  TypeRE: "^Source$"
-  FieldRE: "^Data"