Repository: google/go-flow-levee
Pull request #292 (2021-03-16 14:17:55)

Propagate through calls more selectively to avoid false positives
Specifically, avoid propagating in the absence of knowledge about a function's behavior. As a mitigation, summaries for a large number of standard library functions are added and used, which should give reasonable coverage in the intraprocedural case.

## Context

The impetus for this PR came from issues involving our assumption that, when the function we are analyzing calls another function with a tainted argument, we should assume that the other arguments and the return value are all tainted. This assumption is unworkable in practice, as it leads to frequent false positives, essentially turning the analyzer into a "was a function called with a tainted value" detector. In such cases, the burden of analysis is shifted onto a human, who has to analyze the entire call tree below the "offending" call.

In future work, we will invest in developing an actual interprocedural analysis, probably based on some kind of pointer analysis.

With the changes in this PR, there are no longer any false positives on `kubernetes/kubernetes`.

## Process

I tried to gather standard library functions that I think could reasonably be involved in a propagation. I did not look at all functions. More summaries can easily be added later if desired. Also, 

Packages I covered:
```
fmt
errors
strings
bytes
io
io/ioutil
bufio
context
strconv
encoding/json
encoding/base64
sync
text
log
path
path/filepath
```

## Tips for reviewers

This PR is meant as a single atomic operation that changes the way we propagate through calls. As such, it contains the removal of some tests which are no longer relevant/appropriate. It also contains the introduction of tests for new code.

The core changes are made in `propagation/propagation.go` and the new `propagation/stdlib.go` file. The bulk of the lines added come from `propagation/summaries.go`, which contains the function summaries (these are just data, not code).

- [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


mlevesquedion: Thank you for the thorough review!

Here are my replies to your high-level thoughts:
* propagation package too big? I think it's okay not to split for now.
  - As you mention, most of the lines are from summaries.
  - Logic/purpose of code in stdlib.go feels to me like it's very close to the core propagation logic
  - I may change my mind on this after I've added tests for some of the functions in `stdlib.go`.
* updated documentation (N/A)
  - Agree that we should set expectations in the docs. 
  - I think this can go in an immediately subsequent PR. High level, I'm thinking: analysis is currently intraprocedural, although the analyzer handles some standard library functions, e.g. fmt.Sprintf and any implementation of io.Write. Interprocedural propagation is in the works and expected to land sometime in 2021.


Patch:
diff --git a/internal/pkg/propagation/propagation.go b/internal/pkg/propagation/propagation.go
index b47c71e..80253bf 100644
--- a/internal/pkg/propagation/propagation.go
+++ b/internal/pkg/propagation/propagation.go
@@ -24,7 +24,6 @@ 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/sourcetype"
 	"github.com/google/go-flow-levee/internal/pkg/utils"
 	"golang.org/x/tools/go/pointer"
 	"golang.org/x/tools/go/ssa"
@@ -224,7 +223,6 @@ func (prop *Propagation) taintReferrers(n ssa.Node, maxInstrReached map[*ssa.Bas
 	if !hasTaintableType(n) {
 		return
 	}
-
 	if n.Referrers() == nil {
 		return
 	}
@@ -254,37 +252,7 @@ func (prop *Propagation) taintCall(call *ssa.Call, maxInstrReached map[*ssa.Basi
 		return
 	}
 
-	// Do not traverse through a method call if it is being reached via a Source receiver.
-	// Do traverse if the call is being reached through a tainted argument.
-	// Source methods that return tainted values regardless of their arguments should be identified by the fieldpropagator analyzer.
-	if recv := call.Call.Signature().Recv(); recv != nil && sourcetype.IsSourceType(prop.config, prop.taggedFields, recv.Type()) {
-		// If the receiver is not statically known (it has interface type) and the
-		// method has no arguments, Args will be empty.
-		if len(call.Call.Args) == 0 {
-			return
-		}
-		visitingFromArg := false
-		for i, a := range call.Call.Args {
-			// If the receiver's type is statically known,
-			// it will be the first element of the Args slice.
-			if !call.Call.IsInvoke() && i == 0 {
-				continue
-			}
-			if prop.tainted[a.(ssa.Node)] {
-				visitingFromArg = true
-			}
-		}
-		if !visitingFromArg {
-			// unmark the node so that it may be visited again when taint propagates to an argument
-			prop.tainted[call] = false
-			return
-		}
-	}
-
-	prop.taintReferrers(call, maxInstrReached, lastBlockVisited)
-	for _, a := range call.Call.Args {
-		prop.taintCallArg(a, maxInstrReached, lastBlockVisited)
-	}
+	prop.taintStdlibCall(call, maxInstrReached, lastBlockVisited)
 }
 
 func (prop *Propagation) taintBuiltin(c *ssa.Call, builtinName string, maxInstrReached map[*ssa.BasicBlock]int, lastBlockVisited *ssa.BasicBlock) {
diff --git a/internal/pkg/propagation/stdlib.go b/internal/pkg/propagation/stdlib.go
new file mode 100644
index 0000000..752a886
--- /dev/null
+++ b/internal/pkg/propagation/stdlib.go
@@ -0,0 +1,83 @@
+// 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 propagation
+
+import (
+	"github.com/google/go-flow-levee/internal/pkg/propagation/summary"
+	"golang.org/x/tools/go/ssa"
+)
+
+// taintStdlibCall propagates taint through a static call to a standard
+// library function, or through an implementation of a standard library
+// interface function, provided that the function's taint propagation behavior
+// is known (i.e. the function has a summary).
+func (prop *Propagation) taintStdlibCall(call *ssa.Call, maxInstrReached map[*ssa.BasicBlock]int, lastBlockVisited *ssa.BasicBlock) {
+	summ := summary.For(call)
+	if summ == nil {
+		return
+	}
+
+	var args []ssa.Value
+	// For "invoke" calls, Value is the receiver
+	if call.Call.IsInvoke() {
+		args = append(args, call.Call.Value)
+	}
+	args = append(args, call.Call.Args...)
+
+	// Determine whether we need to propagate taint.
+	tainted := int64(0)
+	for i, a := range args {
+		if prop.tainted[a.(ssa.Node)] {
+			tainted |= 1 << i
+		}
+	}
+	if (tainted & summ.IfTainted) == 0 {
+		return
+	}
+
+	// Taint call arguments.
+	for _, i := range summ.TaintedArgs {
+		prop.taint(args[i].(ssa.Node), maxInstrReached, lastBlockVisited, false)
+	}
+
+	// If there are no referrers, exit early.
+	if call.Referrers() == nil {
+		return
+	}
+
+	// If the call has a single return value, the return value is the call
+	// instruction itself, so if the call's return value is tainted, taint
+	// the Referrers.
+	if call.Call.Signature().Results().Len() == 1 {
+		if len(summ.TaintedRets) > 0 {
+			prop.taintReferrers(call, maxInstrReached, lastBlockVisited)
+		}
+		return
+	}
+
+	// If the call has more than one return value, the call's Referrers will
+	// contain one Extract for each returned value. There is no guarantee that
+	// these will appear in order, so we create a map from the index of
+	// each returned value to the corresponding Extract (the extracted value),
+	// then we taint the Extracts.
+	indexToExtract := map[int]*ssa.Extract{}
+	for _, r := range *call.Referrers() {
+		e := r.(*ssa.Extract)
+		indexToExtract[e.Index] = e
+	}
+	for i := range summ.TaintedRets {
+		prop.taint(indexToExtract[i], maxInstrReached, lastBlockVisited, true)
+	}
+}
diff --git a/internal/pkg/propagation/summary/api.go b/internal/pkg/propagation/summary/api.go
new file mode 100644
index 0000000..88bb66d
--- /dev/null
+++ b/internal/pkg/propagation/summary/api.go
@@ -0,0 +1,133 @@
+// 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 summary provides function summaries for a range of standard
+// library functions that could be involved in a taint propagation.
+// Function summaries describe the taint-propagation behavior of a given
+// function, e.g. "if these arguments are tainted, then the following
+// arguments/return values should also be tainted".
+package summary
+
+import (
+	"go/types"
+	"strings"
+
+	"github.com/google/go-flow-levee/internal/pkg/utils"
+	"golang.org/x/tools/go/ssa"
+)
+
+// For returns the summary for a given call if it exists,
+// or nil if no summary matches the called function.
+func For(call *ssa.Call) *Summary {
+	if summ, ok := funcSummaries[staticFuncName(call)]; ok {
+		return &summ
+	}
+	if summ, ok := interfaceFuncSummaries[funcKey{methodNameWithoutReceiver(call), sigTypeString(call.Call.Signature())}]; ok {
+		return &summ
+	}
+	return nil
+}
+
+// A Summary captures the behavior of a function with respect to taint
+// propagation. Specifically: given that at least one of the necessary
+// arguments is tainted, which arguments/return values become tainted?
+// Note that when it's present, the receiver counts as an argument.
+//
+// As an example, consider fmt.Fprintf:
+//   func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
+// Its Summary is:
+//   "fmt.Fprintf": {
+//   	ifTainted:   0b110,
+//   	taintedArgs: []int{0},
+//   },
+// In English, this says that if the format string or the varargs slice are
+// tainted, then the Writer is tainted.
+// (In an actual summary, 0b110 should be written as second | third for readability.)
+type Summary struct {
+	// IfTainted is a bitset which contains positions for parameters
+	// such that if one of these parameters is tainted, taint should
+	// be propagated to the arguments and return values.
+	// There is a 1-to-1 mapping between the bits and the function's
+	// parameters, with the least significant bit corresponding to the
+	// first (0th) argument.
+	IfTainted int64
+	// the positions of the arguments that taint propagates to if one of the
+	// positions in ifTainted is tainted
+	TaintedArgs []int
+	// the positions of the return values that taint propagates to if one of the
+	// positions in ifTainted is tainted
+	TaintedRets []int
+}
+
+func staticFuncName(call *ssa.Call) string {
+	if sc := call.Call.StaticCallee(); sc != nil {
+		return sc.RelString(call.Parent().Pkg.Pkg)
+	}
+	return ""
+}
+
+func methodNameWithoutReceiver(call *ssa.Call) string {
+	cc := call.Call
+	if cc.IsInvoke() {
+		return cc.Method.Name()
+	}
+	if sc := cc.StaticCallee(); sc != nil {
+		if sc.Signature.Recv() == nil {
+			return ""
+		}
+		return sc.Name()
+	}
+	return ""
+}
+
+// sigTypeString produces a stripped version of a function's signature, containing
+// just the types of the arguments and return values.
+// The receiver's type is not included.
+// For a function such as:
+//   WriteTo(w Writer) (n int64, err error)
+// The result is:
+//   (Writer)(int64,error)
+func sigTypeString(sig *types.Signature) string {
+	var b strings.Builder
+
+	b.WriteByte('(')
+	paramsPtr := sig.Params()
+	if paramsPtr != nil {
+		params := *paramsPtr
+		for i := 0; i < params.Len(); i++ {
+			p := params.At(i)
+			b.WriteString(utils.UnqualifiedName(p))
+			if i+1 != params.Len() {
+				b.WriteByte(',')
+			}
+		}
+	}
+	b.WriteByte(')')
+
+	b.WriteByte('(')
+	resultsPtr := sig.Results()
+	if resultsPtr != nil {
+		results := *resultsPtr
+		for i := 0; i < results.Len(); i++ {
+			p := results.At(i)
+			b.WriteString(utils.UnqualifiedName(p))
+			if i+1 != results.Len() {
+				b.WriteByte(',')
+			}
+		}
+	}
+	b.WriteByte(')')
+
+	return b.String()
+}
diff --git a/internal/pkg/propagation/summary/summaries.go b/internal/pkg/propagation/summary/summaries.go
new file mode 100644
index 0000000..08f44fa
--- /dev/null
+++ b/internal/pkg/propagation/summary/summaries.go
@@ -0,0 +1,744 @@
+// 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 summary provides function summaries for a range of standard
+// library functions that could be involved in a taint propagation.
+// Function summaries describe the taint-propagation behavior of a given
+// function, e.g. "if these arguments are tainted, then the following
+// arguments/return values should also be tainted".
+package summary
+
+const (
+	first = 1 << iota
+	second
+	third
+	fourth
+)
+
+var fromFirstArgToFirstRet = Summary{
+	IfTainted:   first,
+	TaintedRets: []int{0},
+}
+
+// funcSummaries contains summaries for regular functions
+// that could be called statically.
+var funcSummaries = map[string]Summary{
+	// func Errorf(format string, a ...interface{}) error
+	"fmt.Errorf": {
+		IfTainted:   first | second,
+		TaintedRets: []int{0},
+	},
+	// func Sprint(a ...interface{}) string
+	"fmt.Sprint": fromFirstArgToFirstRet,
+	// func Sprintf(format string, a ...interface{}) string
+	"fmt.Sprintf": {
+		IfTainted:   first | second,
+		TaintedRets: []int{0},
+	},
+	// func Sprintln(a ...interface{}) string
+	"fmt.Sprintln": fromFirstArgToFirstRet,
+	// func Fprint(w io.Writer, a ...interface{}) (n int, err error)
+	"fmt.Fprint": {
+		IfTainted:   second,
+		TaintedArgs: []int{0},
+	},
+	// func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error)
+	"fmt.Fprintf": {
+		IfTainted:   second | third,
+		TaintedArgs: []int{0},
+	},
+	// func Fprintln(w io.Writer, a ...interface{}) (n int, err error)
+	"fmt.Fprintln": {
+		IfTainted:   second,
+		TaintedArgs: []int{0},
+	},
+	// func Sscan(str string, a ...interface{}) (n int, err error)
+	"fmt.Sscan": {
+		IfTainted:   first,
+		TaintedArgs: []int{1},
+	},
+	// func Sscanln(str string, a ...interface{}) (n int, err error)
+	"fmt.Sscanln": {
+		IfTainted:   first,
+		TaintedArgs: []int{1},
+	},
+	// func Sscanf(str string, format string, a ...interface{}) (n int, err error)
+	"fmt.Sscanf": {
+		IfTainted:   first,
+		TaintedArgs: []int{2},
+	},
+	// func Fscan(r io.Reader, a ...interface{}) (n int, err error)
+	"fmt.Fscan": {
+		IfTainted:   first,
+		TaintedArgs: []int{1},
+	},
+	// func Fscanln(r io.Reader, a ...interface{}) (n int, err error)
+	"fmt.Fscanln": {
+		IfTainted:   first,
+		TaintedArgs: []int{1},
+	},
+	// func Fscanf(r io.Reader, format string, a ...interface{}) (n int, err error)
+	"fmt.Fscanf": {
+		IfTainted:   first,
+		TaintedArgs: []int{2},
+	},
+	// func New(text string) error
+	"errors.New": fromFirstArgToFirstRet,
+	// func Unwrap(err error) error
+	"errors.Unwrap": fromFirstArgToFirstRet,
+	// func As(err error, target interface{}) bool
+	"errors.As": {
+		IfTainted:   first,
+		TaintedArgs: []int{1},
+	},
+	// func SplitN(s, sep string, n int) []string
+	"strings.SplitN": fromFirstArgToFirstRet,
+	// func SplitAfterN(s, sep string, n int) []string
+	"strings.SplitAfterN": fromFirstArgToFirstRet,
+	// func Split(s, sep string) []string
+	"strings.Split": fromFirstArgToFirstRet,
+	// func SplitAfter(s, sep string) []string
+	"strings.SplitAfter": fromFirstArgToFirstRet,
+	// func Fields(s string) []string
+	"strings.Fields": fromFirstArgToFirstRet,
+	// func FieldsFunc(s string, f func(rune) bool) []string
+	"strings.FieldsFunc": fromFirstArgToFirstRet,
+	// func Join(elems []string, sep string) string
+	"strings.Join": {
+		IfTainted:   first | second,
+		TaintedRets: []int{0},
+	},
+	// func Map(mapping func(rune) rune, s string) string
+	"strings.Map": {
+		IfTainted:   second,
+		TaintedRets: []int{0},
+	},
+	// func Repeat(s string, count int) string
+	"strings.Repeat": fromFirstArgToFirstRet,
+	// func ToUpper(s string) string
+	"strings.ToUpper": fromFirstArgToFirstRet,
+	// func ToLower(s string) string
+	"strings.ToLower": fromFirstArgToFirstRet,
+	// func ToTitle(s string) string
+	"strings.ToTitle": fromFirstArgToFirstRet,
+	// func ToUpperSpecial(c unicode.SpecialCase, s string) string
+	"strings.ToUpperSpecial": {
+		IfTainted:   second,
+		TaintedRets: []int{0},
+	},
+	// func ToLowerSpecial(c unicode.SpecialCase, s string) string
+	"strings.ToLowerSpecial": {
+		IfTainted:   second,
+		TaintedRets: []int{0},
+	},
+	// func ToTitleSpecial(c unicode.SpecialCase, s string) string
+	"strings.ToTitleSpecial": {
+		IfTainted:   second,
+		TaintedRets: []int{0},
+	},
+	// func ToValidUTF8(s, replacement string) string
+	"strings.ToValidUTF8": {
+		IfTainted:   first | second,
+		TaintedRets: []int{0},
+	},
+	// func Title(s string) string
+	"strings.Title": fromFirstArgToFirstRet,
+	// func TrimLeftFunc(s string, f func(rune) bool) string
+	"strings.TrimLeftFunc": fromFirstArgToFirstRet,
+	// func TrimRightFunc(s string, f func(rune) bool) string
+	"strings.TrimRightFunc": fromFirstArgToFirstRet,
+	// func TrimFunc(s string, f func(rune) bool) string
+	"strings.TrimFunc": fromFirstArgToFirstRet,
+	// func Trim(s, cutset string) string
+	"strings.Trim": fromFirstArgToFirstRet,
+	// func TrimLeft(s, cutset string) string
+	"strings.TrimLeft": fromFirstArgToFirstRet,
+	// func TrimRight(s, cutset string) string
+	"strings.TrimRight": fromFirstArgToFirstRet,
+	// func TrimSpace(s string) string
+	"strings.TrimSpace": fromFirstArgToFirstRet,
+	// func TrimPrefix(s, prefix string) string
+	"strings.TrimPrefix": fromFirstArgToFirstRet,
+	// func TrimSuffix(s, suffix string) string
+	"strings.TrimSuffix": fromFirstArgToFirstRet,
+	// func Replace(s, old, new string, n int) string
+	"strings.Replace": {
+		IfTainted:   first | third,
+		TaintedRets: []int{0},
+	},
+	// func ReplaceAll(s, old, new string) string
+	"strings.ReplaceAll": {
+		IfTainted:   first | third,
+		TaintedRets: []int{0},
+	},
+	// func NewReader(s string) *Reader
+	"strings.NewReader": fromFirstArgToFirstRet,
+	// func (r *Replacer) Replace(s string) string
+	"(*strings.Replacer).Replace": {
+		IfTainted:   first | second,
+		TaintedRets: []int{0},
+	},
+	// func (r *Replacer) WriteString(w io.Writer, s string) (n int, err error)
+	"(*strings.Replacer).WriteString": {
+		IfTainted:   first | third,
+		TaintedArgs: []int{1},
+	},
+	// func NewReplacer(oldnew ...string) *Replacer
+	"strings.NewReplacer": fromFirstArgToFirstRet,
+	// func (b *Buffer) Next(n int) []byte
+	"(*bytes.Buffer).Next": fromFirstArgToFirstRet,
+	// func (b *Buffer) ReadBytes(delim byte) (line []byte, err error)
+	"(*bytes.Buffer).ReadBytes": fromFirstArgToFirstRet,
+	// func (b *Buffer) ReadString(delim byte) (line string, err error)
+	"(*bytes.Buffer).ReadString": fromFirstArgToFirstRet,
+	// func NewBuffer(buf []byte) *Buffer
+	"bytes.NewBuffer": fromFirstArgToFirstRet,
+	// func NewBufferString(s string) *Buffer
+	"bytes.NewBufferString": fromFirstArgToFirstRet,
+	// func SplitN(s, sep []byte, n int) [][]byte
+	"bytes.SplitN": fromFirstArgToFirstRet,
+	// func SplitAfterN(s, sep []byte, n int) [][]byte
+	"bytes.SplitAfterN": fromFirstArgToFirstRet,
+	// func Split(s, sep []byte) [][]byte
+	"bytes.Split": fromFirstArgToFirstRet,
+	// func SplitAfter(s, sep []byte) [][]byte
+	"bytes.SplitAfter": fromFirstArgToFirstRet,
+	// func Fields(s []byte) [][]byte
+	"bytes.Fields": fromFirstArgToFirstRet,
+	// func FieldsFunc(s []byte, f func(rune) bool) [][]byte
+	"bytes.FieldsFunc": fromFirstArgToFirstRet,
+	// func Join(s [][]byte, sep []byte) []byte
+	"bytes.Join": {
+		IfTainted:   first | second,
+		TaintedRets: []int{0},
+	},
+	// func Map(mapping func(r rune) rune, s []byte) []byte
+	"bytes.Map": {
+		IfTainted:   second,
+		TaintedRets: []int{0},
+	},
+	// func Repeat(b []byte, count int) []byte
+	"bytes.Repeat": fromFirstArgToFirstRet,
+	// func ToUpper(s []byte) []byte
+	"bytes.ToUpper": fromFirstArgToFirstRet,
+	// func ToLower(s []byte) []byte
+	"bytes.ToLower": fromFirstArgToFirstRet,
+	// func ToTitle(s []byte) []byte
+	"bytes.ToTitle": fromFirstArgToFirstRet,
+	// func ToUpperSpecial(c unicode.SpecialCase, s []byte) []byte
+	"bytes.ToUpperSpecial": {
+		IfTainted:   second,
+		TaintedRets: []int{0},
+	},
+	// func ToLowerSpecial(c unicode.SpecialCase, s []byte) []byte
+	"bytes.ToLowerSpecial": {
+		IfTainted:   second,
+		TaintedRets: []int{0},
+	},
+	// func ToTitleSpecial(c unicode.SpecialCase, s []byte) []byte
+	"bytes.ToTitleSpecial": {
+		IfTainted:   second,
+		TaintedRets: []int{0},
+	},
+	// func ToValidUTF8(s, replacement []byte) []byte
+	"bytes.ToValidUTF8": {
+		IfTainted:   first | second,
+		TaintedRets: []int{0},
+	},
+	// func Title(s []byte) []byte
+	"bytes.Title": fromFirstArgToFirstRet,
+	// func TrimLeftFunc(s []byte, f func(r rune) bool) []byte
+	"bytes.TrimLeftFunc": fromFirstArgToFirstRet,
+	// func TrimRightFunc(s []byte, f func(r rune) bool) []byte
+	"bytes.TrimRightFunc": fromFirstArgToFirstRet,
+	// func TrimFunc(s []byte, f func(r rune) bool) []byte
+	"bytes.TrimFunc": fromFirstArgToFirstRet,
+	// func TrimPrefix(s, prefix []byte) []byte
+	"bytes.TrimPrefix": fromFirstArgToFirstRet,
+	// func TrimSuffix(s, suffix []byte) []byte
+	"bytes.TrimSuffix": fromFirstArgToFirstRet,
+	// func Trim(s []byte, cutset string) []byte
+	"bytes.Trim": fromFirstArgToFirstRet,
+	// func TrimLeft(s []byte, cutset string) []byte
+	"bytes.TrimLeft": fromFirstArgToFirstRet,
+	// func TrimRight(s []byte, cutset string) []byte
+	"bytes.TrimRight": fromFirstArgToFirstRet,
+	// func TrimSpace(s []byte) []byte
+	"bytes.TrimSpace": fromFirstArgToFirstRet,
+	// func Runes(s []byte) []rune
+	"bytes.Runes": fromFirstArgToFirstRet,
+	// func Replace(s, old, new []byte, n int) []byte
+	"bytes.Replace": {
+		IfTainted:   first | third,
+		TaintedRets: []int{0},
+	},
+	// func ReplaceAll(s, old, new []byte) []byte
+	"bytes.ReplaceAll": {
+		IfTainted:   first | third,
+		TaintedRets: []int{0},
+	},
+	// func NewReader(b []byte) *Reader
+	"bytes.NewReader": fromFirstArgToFirstRet,
+	// func WriteString(w Writer, s string) (n int, err error)
+	"io.WriteString": {
+		IfTainted:   second,
+		TaintedArgs: []int{0},
+	},
+	// func ReadAtLeast(r Reader, buf []byte, min int) (n int, err error)
+	"io.ReadAtLeast": {
+		IfTainted:   first,
+		TaintedArgs: []int{1},
+	},
+	// func ReadFull(r Reader, buf []byte) (n int, err error)
+	"io.ReadFull": {
+		IfTainted:   first,
+		TaintedArgs: []int{1},
+	},
+	// func CopyN(dst Writer, src Reader, n int64) (written int64, err error)
+	"io.CopyN": {
+		IfTainted:   second,
+		TaintedArgs: []int{0},
+	},
+	// func Copy(dst Writer, src Reader) (written int64, err error)
+	"io.Copy": {
+		IfTainted:   second,
+		TaintedArgs: []int{0},
+	},
+	// func CopyBuffer(dst Writer, src Reader, buf []byte) (written int64, err error)
+	"io.CopyBuffer": {
+		IfTainted:   second,
+		TaintedArgs: []int{0, 2},
+	},
+	// func LimitReader(r Reader, n int64) Reader
+	"io.LimitReader": fromFirstArgToFirstRet,
+	// func TeeReader(r Reader, w Writer) Reader
+	"io.TeeReader": {
+		IfTainted:   first | second,
+		TaintedRets: []int{0},
+	},
+	// func MultiReader(readers ...Reader) Reader
+	"io.MultiReader": fromFirstArgToFirstRet,
+	// func MultiWriter(writers ...Writer) Writer
+	"io.MultiWriter": fromFirstArgToFirstRet,
+	// func (r *PipeReader) CloseWithError(err error) error
+	"(*io.PipeReader).CloseWithError": fromFirstArgToFirstRet,
+	// func (w *PipeWriter) CloseWithError(err error) error
+	"(*io.PipeWriter).CloseWithError": fromFirstArgToFirstRet,
+	// func ReadAll(r io.Reader) ([]byte, error)
+	"io/ioutil.ReadAll": fromFirstArgToFirstRet,
+	// func NopCloser(r io.Reader) io.ReadCloser
+	"io/ioutil.NopCloser": fromFirstArgToFirstRet,
+	// func NewReaderSize(rd io.Reader, size int) *Reader
+	"bufio.NewReaderSize": fromFirstArgToFirstRet,
+	// func NewReader(rd io.Reader) *Reader
+	"bufio.NewReader": fromFirstArgToFirstRet,
+	// func (b *Reader) Peek(n int) ([]byte, error)
+	"(*bufio.Reader).Peek": fromFirstArgToFirstRet,
+	// func (b *Reader) ReadSlice(delim byte) (line []byte, err error)
+	"(*bufio.Reader).ReadSlice": fromFirstArgToFirstRet,
+	// func (b *Reader) ReadLine() (line []byte, isPrefix bool, err error)
+	"(*bufio.Reader).ReadLine": fromFirstArgToFirstRet,
+	// func (b *Reader) ReadBytes(delim byte) ([]byte, error)
+	"(*bufio.Reader).ReadBytes": fromFirstArgToFirstRet,
+	// func (b *Reader) ReadString(delim byte) (string, error)
+	"(*bufio.Reader).ReadString": fromFirstArgToFirstRet,
+	// func NewWriterSize(w io.Writer, size int) *Writer
+	"bufio.NewWriterSize": fromFirstArgToFirstRet,
+	// func NewWriter(w io.Writer) *Writer
+	"bufio.NewWriter": fromFirstArgToFirstRet,
+	// func NewReadWriter(r *Reader, w *Writer) *ReadWriter
+	"bufio.NewReadWriter": {
+		IfTainted:   first | second,
+		TaintedRets: []int{0},
+	},
+	// func NewScanner(r io.Reader) *Scanner
+	"bufio.NewScanner": fromFirstArgToFirstRet,
+	// func (s *Scanner) Bytes() []byte
+	"(*bufio.Scanner).Bytes": fromFirstArgToFirstRet,
+	// func (s *Scanner) Text() string
+	"(*bufio.Scanner).Text": fromFirstArgToFirstRet,
+	// func (s *Scanner) Buffer(buf []byte, max int)
+	"(*bufio.Scanner).Buffer": {
+		IfTainted:   second,
+		TaintedArgs: []int{0},
+	},
+	// func ScanLines(data []byte, atEOF bool) (advance int, token []byte, err error)
+	"bufio.ScanLines": {
+		IfTainted:   first,
+		TaintedRets: []int{1},
+	},
+	// func ScanWords(data []byte, atEOF bool) (advance int, token []byte, err error)
+	"bufio.ScanWords": {
+		IfTainted:   first,
+		TaintedRets: []int{1},
+	},
+	// func WithValue(parent Context, key, val interface{}) Context
+	"context.WithValue": {
+		IfTainted:   first | second | third,
+		TaintedRets: []int{0},
+	},
+	// func AppendBool(dst []byte, b bool) []byte
+	"strconv.AppendBool": fromFirstArgToFirstRet,
+	// func AppendFloat(dst []byte, f float64, fmt byte, prec, bitSize int) []byte
+	"strconv.AppendFloat": fromFirstArgToFirstRet,
+	// func AppendInt(dst []byte, i int64, base int) []byte
+	"strconv.AppendInt": fromFirstArgToFirstRet,
+	// func AppendUint(dst []byte, i uint64, base int) []byte
+	"strconv.AppendUint": fromFirstArgToFirstRet,
+	// func Quote(s string) string
+	"strconv.Quote": fromFirstArgToFirstRet,
+	// func AppendQuote(dst []byte, s string) []byte
+	"strconv.AppendQuote": {
+		IfTainted:   first | second,
+		TaintedRets: []int{0},
+	},
+	// func QuoteToASCII(s string) string
+	"strconv.QuoteToASCII": fromFirstArgToFirstRet,
+	// func AppendQuoteToASCII(dst []byte, s string) []byte
+	"strconv.AppendQuoteToASCII": {
+		IfTainted:   first | second,
+		TaintedRets: []int{0},
+	},
+	// func QuoteToGraphic(s string) string
+	"strconv.QuoteToGraphic": fromFirstArgToFirstRet,
+	// func AppendQuoteToGraphic(dst []byte, s string) []byte
+	"strconv.AppendQuoteToGraphic": {
+		IfTainted:   first | second,
+		TaintedRets: []int{0},
+	},
+	// func AppendQuoteRune(dst []byte, r rune) []byte
+	"strconv.AppendQuoteRune": fromFirstArgToFirstRet,
+	// func AppendQuoteRuneToASCII(dst []byte, r rune) []byte
+	"strconv.AppendQuoteRuneToASCII": fromFirstArgToFirstRet,
+	// func AppendQuoteRuneToGraphic(dst []byte, r rune) []byte
+	"strconv.AppendQuoteRuneToGraphic": fromFirstArgToFirstRet,
+	// func UnquoteChar(s string, quote byte) (value rune, multibyte bool, tail string, err error)
+	"strconv.UnquoteChar": {
+		IfTainted:   first,
+		TaintedRets: []int{2},
+	},
+	// func Unquote(s string) (string, error)
+	"strconv.Unquote": fromFirstArgToFirstRet,
+	// func Unmarshal(data []byte, v interface{}) error
+	"encoding/json.Unmarshal": {
+		IfTainted:   first | second,
+		TaintedArgs: []int{0, 1},
+	},
+	// func Marshal(v interface{}) ([]byte, error)
+	"encoding/json.Marshal": fromFirstArgToFirstRet,
+	// func MarshalIndent(v interface{}, prefix, indent string) ([]byte, error)
+	"encoding/json.MarshalIndent": fromFirstArgToFirstRet,
+	// func HTMLEscape(dst *bytes.Buffer, src []byte)
+	"encoding/json.HTMLEscape": {
+		IfTainted:   second,
+		TaintedArgs: []int{0},
+	},
+	// func Compact(dst *bytes.Buffer, src []byte) error
+	"encoding/json.Compact": {
+		IfTainted:   second,
+		TaintedArgs: []int{0},
+	},
+	// func Indent(dst *bytes.Buffer, src []byte, prefix, indent string) error
+	"encoding/json.Indent": {
+		IfTainted:   second,
+		TaintedArgs: []int{0},
+	},
+	// func NewDecoder(r io.Reader) *Decoder
+	"encoding/json.NewDecoder": fromFirstArgToFirstRet,
+	// func (dec *Decoder) Decode(v interface{}) error
+	"(*encoding/json.Decoder).Decode": {
+		IfTainted:   first,
+		TaintedArgs: []int{1},
+	},
+	// func (dec *Decoder) Buffered() io.Reader
+	"(*encoding/json.Decoder).Buffered": fromFirstArgToFirstRet,
+	// func (dec *Decoder) Token() (Token, error)
+	"(*encoding/json.Decoder).Token": fromFirstArgToFirstRet,
+	// func NewEncoder(w io.Writer) *Encoder
+	"encoding/json.NewEncoder": fromFirstArgToFirstRet,
+	// func (enc *Encoder) Encode(v interface{}) error
+	"(*encoding/json.Encoder).Encode": {
+		IfTainted:   second,
+		TaintedArgs: []int{0},
+	},
+	// func (m RawMessage) MarshalJSON() ([]byte, error)
+	"(encoding/json.RawMessage).MarshalJSON": fromFirstArgToFirstRet,
+	// func (m *RawMessage) UnmarshalJSON(data []byte) error
+	"(*encoding/json.RawMessage).UnmarshalJSON": {
+		IfTainted:   second,
+		TaintedArgs: []int{0},
+	},
+	// func (enc *Encoding) Encode(dst, src []byte)
+	"(*encoding/base64.Encoding).Encode": {
+		IfTainted:   second,
+		TaintedArgs: []int{0},
+	},
+	// func (enc *Encoding) EncodeToString(src []byte) string
+	"(*encoding/base64.Encoding).EncodeToString": fromFirstArgToFirstRet,
+	// func (enc *Encoding) DecodeString(s string) ([]byte, error)
+	"(*encoding/base64.Encoding).DecodeString": fromFirstArgToFirstRet,
+	// func (enc *Encoding) Decode(dst, src []byte) (n int, err error)
+	"(*encoding/base64.Encoding).Decode": {
+		IfTainted:   second,
+		TaintedArgs: []int{0},
+	},
+	// func NewDecoder(enc *Encoding, r io.Reader) io.Reader
+	"encoding/base64.NewDecoder": fromFirstArgToFirstRet,
+	// func (m *Map) Load(key interface{}) (value interface{}, ok bool)
+	"(*sync.Map).Load": fromFirstArgToFirstRet,
+	// func (m *Map) Store(key, value interface{})
+	"(*sync.Map).Store": {
+		IfTainted:   second | third,
+		TaintedArgs: []int{0},
+	},
+	// func (m *Map) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool)
+	"(*sync.Map).LoadOrStore": {
+		IfTainted:   first | second | third,
+		TaintedArgs: []int{0},
+		TaintedRets: []int{0},
+	},
+	// func (m *Map) LoadAndDelete(key interface{}) (value interface{}, loaded bool)
+	"(*sync.Map).LoadAndDelete": fromFirstArgToFirstRet,
+	// func (p *Pool) Put(x interface{})
+	"(*sync.Pool).Put": {
+		IfTainted:   second,
+		TaintedArgs: []int{0},
+	},
+	// func (p *Pool) Get() interface{}
+	"(*sync.Pool).Get": fromFirstArgToFirstRet,
+	// func (s *Scanner) Init(src io.Reader) *Scanner
+	"(*text/scanner.Scanner).Init": {
+		IfTainted:   second,
+		TaintedArgs: []int{0},
+		TaintedRets: []int{0},
+	},
+	// func (s *Scanner) TokenText() string
+	"(*text/scanner.Scanner).TokenText": fromFirstArgToFirstRet,
+	// func (b *Writer) Write(buf []byte) (n int, err error)
+	"(*text/tabwriter.Writer).Write": {
+		IfTainted:   second,
+		TaintedArgs: []int{0},
+	},
+	// func NewWriter(output io.Writer, minwidth, tabwidth, padding int, padchar byte, flags uint) *Writer
+	"text/tabwriter.NewWriter": fromFirstArgToFirstRet,
+	// func (t *Template) ExecuteTemplate(wr io.Writer, name string, data interface{}) error
+	"(*text/template.Template).ExecuteTemplate": {
+		IfTainted:   fourth,
+		TaintedArgs: []int{1},
+	},
+	// func (t *Template) Execute(wr io.Writer, data interface{}) error
+	"(*text/template.Template).Execute": {
+		IfTainted:   third,
+		TaintedArgs: []int{1},
+	},
+	// func HTMLEscape(w io.Writer, b []byte)
+	"text/template.HTMLEscape": {
+		IfTainted:   second,
+		TaintedArgs: []int{0},
+	},
+	// func HTMLEscapeString(s string) string
+	"text/template.HTMLEscapeString": fromFirstArgToFirstRet,
+	// func HTMLEscaper(args ...interface{}) string
+	"text/template.HTMLEscaper": fromFirstArgToFirstRet,
+	// func JSEscape(w io.Writer, b []byte)
+	"text/template.JSEscape": {
+		IfTainted:   010,
+		TaintedArgs: []int{0},
+	},
+	// func JSEscapeString(s string) string
+	"text/template.JSEscapeString": fromFirstArgToFirstRet,
+	// func JSEscaper(args ...interface{}) string
+	"text/template.JSEscaper": fromFirstArgToFirstRet,
+	// func URLQueryEscaper(args ...interface{}) string
+	"text/template.URLQueryEscaper": fromFirstArgToFirstRet,
+	// func (t *Template) ExecuteTemplate(wr io.Writer, name string, data interface{}) error
+	"(*html/template.Template).ExecuteTemplate": {
+		IfTainted:   fourth,
+		TaintedArgs: []int{1},
+	},
+	// func (t *Template) Execute(wr io.Writer, data interface{}) error
+	"(*html/template.Template).Execute": {
+		IfTainted:   third,
+		TaintedArgs: []int{1},
+	},
+	// func HTMLEscape(w io.Writer, b []byte)
+	"html/template.HTMLEscape": {
+		IfTainted:   second,
+		TaintedArgs: []int{0},
+	},
+	// func HTMLEscapeString(s string) string
+	"html/template.HTMLEscapeString": fromFirstArgToFirstRet,
+	// func HTMLEscaper(args ...interface{}) string
+	"html/template.HTMLEscaper": fromFirstArgToFirstRet,
+	// func JSEscape(w io.Writer, b []byte)
+	"html/template.JSEscape": {
+		IfTainted:   010,
+		TaintedArgs: []int{0},
+	},
+	// func JSEscapeString(s string) string
+	"html/template.JSEscapeString": fromFirstArgToFirstRet,
+	// func JSEscaper(args ...interface{}) string
+	"html/template.JSEscaper": fromFirstArgToFirstRet,
+	// func URLQueryEscaper(args ...interface{}) string
+	"html/template.URLQueryEscaper": fromFirstArgToFirstRet,
+	// func Clean(path string) string
+	"path.Clean": fromFirstArgToFirstRet,
+	// func Split(path string) (dir, file string)
+	"path.Split": {
+		IfTainted:   first,
+		TaintedRets: []int{0, 1},
+	},
+	// func Join(elem ...string) string
+	"path.Join": fromFirstArgToFirstRet,
+	// func Base(path string) string
+	"path.Base": fromFirstArgToFirstRet,
+	// func Clean(path string) string
+	"path/filepath.Clean": fromFirstArgToFirstRet,
+	// func ToSlash(path string) string
+	"path/filepath.ToSlash": fromFirstArgToFirstRet,
+	// func FromSlash(path string) string
+	"path/filepath.FromSlash": fromFirstArgToFirstRet,
+	// func SplitList(path string) []string
+	"path/filepath.SplitList": fromFirstArgToFirstRet,
+	// func Split(path string) (dir, file string)
+	"path/filepath.Split": {
+		IfTainted:   first,
+		TaintedRets: []int{0, 1},
+	},
+	// func Join(elem ...string) string
+	"path/filepath.Join": fromFirstArgToFirstRet,
+	// func Ext(path string) string
+	"path/filepath.Ext": fromFirstArgToFirstRet,
+	// func Abs(path string) (string, error)
+	"path/filepath.Abs": fromFirstArgToFirstRet,
+	// func Base(path string) string
+	"path/filepath.Base": fromFirstArgToFirstRet,
+	// func New(out io.Writer, prefix string, flag int) *Logger
+	"log.New": fromFirstArgToFirstRet,
+	// func (l *Logger) SetOutput(w io.Writer)
+	"(*log.Logger).SetOutput": {
+		IfTainted:   second,
+		TaintedArgs: []int{0},
+	},
+	// func (l *Logger) Writer() io.Writer
+	"(*log.Logger).Writer": fromFirstArgToFirstRet,
+}
+
+// funcKey represents an interface function by its name and its signature.
+// The signature is a string representation containing only the types of
+// the arguments and return values.
+type funcKey struct {
+	name, signature string
+}
+
+// interfaceFuncSummaries contains summaries for common interface functions
+// such as Write or Read, that could be called statically (i.e. a call to
+// a concrete method whose signature matches an interface method) or dynamically
+// (i.e. a call to an interface method on an interface value).
+// Since all of these functions have receivers, the "first" argument in `ifTainted`
+// always corresponds to the receiver.
+var interfaceFuncSummaries = map[funcKey]Summary{
+	// type io.Reader interface {
+	//  Read(p []byte) (n int, err error)
+	// }
+	{"Read", "([]byte)(int,error)"}: {
+		IfTainted:   first,
+		TaintedArgs: []int{1},
+	},
+	// type io.Writer interface {
+	//  Write(p []byte) (n int, err error)
+	// }
+	{"Write", "([]byte)(int,error)"}: {
+		IfTainted:   second,
+		TaintedArgs: []int{0},
+	},
+	// type io.ReaderFrom interface {
+	//  ReadFrom(r Reader) (n int64, err error)
+	// }
+	{"ReadFrom", "(Reader)(int64,error)"}: {
+		IfTainted:   second,
+		TaintedArgs: []int{0},
+	},
+	// type io.WriterTo interface {
+	//  WriteTo(w Writer) (n int64, err error)
+	// }
+	{"WriteTo", "(Writer)(int64,error)"}: {
+		IfTainted:   first,
+		TaintedArgs: []int{1},
+	},
+	// type io.ReaderAt interface {
+	//  ReadAt(p []byte, off int64) (n int, err error)
+	// }
+	{"ReadAt", "([]byte,int64)(int,error)"}: {
+		IfTainted:   first,
+		TaintedArgs: []int{1},
+	},
+	// type io.WriterAt interface {
+	//  WriteAt(p []byte, off int64) (n int, err error)
+	// }
+	{"WriteAt", "([]byte,int64)(int,error)"}: {
+		IfTainted:   second,
+		TaintedArgs: []int{0},
+	},
+	// type io.StringWriter interface {
+	//  WriteString(s string) (n int, err error)
+	// }
+	{"WriteString", "(string)(int,error)"}: {
+		IfTainted:   second,
+		TaintedArgs: []int{0},
+	},
+	// type fmt.Stringer interface {
+	//  String() string
+	// }
+	{"String", "()(string)"}: {
+		IfTainted:   first,
+		TaintedRets: []int{0},
+	},
+	// type fmt.GoStringer interface {
+	//  GoString() string
+	// }
+	{"GoString", "()(string)"}: {
+		IfTainted:   first,
+		TaintedRets: []int{0},
+	},
+	// type error interface {
+	//  Error() string
+	// }
+	{"Error", "()(string)"}: {
+		IfTainted:   first,
+		TaintedRets: []int{0},
+	},
+	// Unwrap() error
+	{"Unwrap", "()(error)"}: {
+		IfTainted:   first,
+		TaintedRets: []int{0},
+	},
+	// Bytes() []byte
+	{"Bytes", "()([]byte)"}: {
+		IfTainted:   first,
+		TaintedRets: []int{0},
+	},
+	// type context.Context interface {
+	//  Err() error
+	//  Value(key interface{}) interface{}
+	// }
+	{"Err", "()(error)"}: {
+		IfTainted:   first,
+		TaintedRets: []int{0},
+	},
+	{"Value", "(interface{})(interface{})"}: {
+		IfTainted:   first,
+		TaintedRets: []int{0},
+	},
+}
diff --git a/internal/pkg/utils/utils.go b/internal/pkg/utils/utils.go
index 21cc3f8..a042798 100644
--- a/internal/pkg/utils/utils.go
+++ b/internal/pkg/utils/utils.go
@@ -17,7 +17,6 @@ package utils
 
 import (
 	"go/types"
-	"strings"
 
 	"golang.org/x/tools/go/ssa"
 )
@@ -60,13 +59,12 @@ func DecomposeField(t types.Type, field int) (typePath, typeName, fieldName stri
 	return
 }
 
-func unqualifiedName(v *types.Var) string {
-	packageQualifiedName := v.Type().String()
-	dotPos := strings.LastIndexByte(packageQualifiedName, '.')
-	if dotPos == -1 {
-		return packageQualifiedName
-	}
-	return packageQualifiedName[dotPos+1:]
+// UnqualifiedName returns the name of the given type, without the qualifying
+// prefix containing the package in which it was declared.
+// Example: for a type named T declared in package p, the returned string will
+// be just `T` instead of `p.T`.
+func UnqualifiedName(t *types.Var) string {
+	return types.TypeString(t.Type(), func(*types.Package) string { return "" })
 }
 
 // DecomposeFunction returns the path, receiver, and name strings of a ssa.Function.
@@ -79,7 +77,7 @@ func DecomposeFunction(f *ssa.Function) (path, recv, name string) {
 	}
 	name = f.Name()
 	if recvVar := f.Signature.Recv(); recvVar != nil {
-		recv = unqualifiedName(recvVar)
+		recv = UnqualifiedName(recvVar)
 	}
 	return
 }


Test patch:
diff --git a/internal/pkg/levee/testdata/src/levee_analysistest/example/core/source.go b/internal/pkg/levee/testdata/src/levee_analysistest/example/core/source.go
index 048e450..ba19f85 100644
--- a/internal/pkg/levee/testdata/src/levee_analysistest/example/core/source.go
+++ b/internal/pkg/levee/testdata/src/levee_analysistest/example/core/source.go
@@ -40,10 +40,6 @@ func (s Source) CopyPointer() (*Source, error) {
 	return &s, nil
 }
 
-func (s Source) Propagate(str string) string {
-	return str
-}
-
 type TaggedSource struct {
 	Data string `levee:"source"`
 	ID   int
@@ -62,7 +58,3 @@ func (i Innocuous) GetID() int {
 func (i Innocuous) GetData() string {
 	return i.Data
 }
-
-type SourceManipulator interface {
-	Propagate(string) string
-}
diff --git a/internal/pkg/levee/testdata/src/levee_analysistest/example/tests/callorder/colocation.go b/internal/pkg/levee/testdata/src/levee_analysistest/example/tests/callorder/colocation.go
index 7ee916e..ba9b58b 100644
--- a/internal/pkg/levee/testdata/src/levee_analysistest/example/tests/callorder/colocation.go
+++ b/internal/pkg/levee/testdata/src/levee_analysistest/example/tests/callorder/colocation.go
@@ -15,56 +15,31 @@
 package callorder
 
 import (
+	"fmt"
+	"io"
 	"levee_analysistest/example/core"
 )
 
-func TestTaintedColocatedArgumentDoesNotReachSinkThatPrecedesColocation() {
-	s := core.Source{}
-	i := newInnocuous()
-	if err := fail(i); err != nil {
-		core.Sink(err)
+func TestTaintedColocatedArgumentDoesNotReachSinkThatPrecedesColocation(w io.Writer, src core.Source) {
+	if true {
+		core.Sink(w)
 	}
-	taintColocated(s, i)
+	fmt.Fprint(w, src)
 }
 
-func TestTaintedColocatedArgumentReachesSinkThatFollowsColocation() {
-	s := core.Source{}
-	i := newInnocuous()
-	taintColocated(s, i)
-	if err := fail(i); err != nil {
-		core.Sink(err) // want "a source has reached a sink"
+func TestTaintedColocatedArgumentReachesSinkThatFollowsColocation(w io.Writer, src core.Source) {
+	if _, err := fmt.Fprint(w, src); err != nil {
+		core.Sink(w) // want "a source has reached a sink"
 	}
 }
 
-func TestAvoidingIncorrectPropagationFromColocationDoesNotPreventCorrectReport() {
-	source := newSource()
-
-	cp, err := copy(source)
+func TestAvoidingIncorrectPropagationFromColocationDoesNotPreventCorrectReport(w io.Writer, src core.Source) {
+	_, err := fmt.Fprint(w, src)
 	if err != nil {
-		core.Sink(err) // want "a source has reached a sink"
+		core.Sink(w) // want "a source has reached a sink"
 	}
 
 	if true {
-		innoc := newInnocuous()
-		taintColocated(cp, innoc)
+		fmt.Fprint(w, src)
 	}
 }
-
-func fail(x interface{}) error {
-	return nil
-}
-
-func taintColocated(a, b interface{}) {
-}
-
-func newInnocuous() *core.Innocuous {
-	return &core.Innocuous{}
-}
-
-func newSource() *core.Source {
-	return &core.Source{}
-}
-
-func copy(a interface{}) (interface{}, error) {
-	return nil, nil
-}
diff --git a/internal/pkg/levee/testdata/src/levee_analysistest/example/tests/colocation/tests.go b/internal/pkg/levee/testdata/src/levee_analysistest/example/tests/colocation/tests.go
index 0b0ff7e..d463fd3 100644
--- a/internal/pkg/levee/testdata/src/levee_analysistest/example/tests/colocation/tests.go
+++ b/internal/pkg/levee/testdata/src/levee_analysistest/example/tests/colocation/tests.go
@@ -16,120 +16,10 @@ package colocation
 
 import (
 	"encoding/json"
-	"reflect"
 
 	"levee_analysistest/example/core"
 )
 
-func colocateString(core.Source, string) {}
-
-func TestBasicTypeIsNotTainted(s core.Source, str string) {
-	colocateString(s, str)
-	core.Sink(str)
-}
-
-func colocateStringPointer(core.Source, *string) {}
-
-func TestBasicPointerTypeIsTainted(s core.Source, strptr *string) {
-	colocateStringPointer(s, strptr)
-	core.Sink(strptr) // want "a source has reached a sink"
-}
-
-func TestPointerToBasicTypeIsTainted(s core.Source, str string) {
-	// This test is failing because &x introduces an Alloc,
-	// and we don't traverse through non-array Allocs
-	colocateStringPointer(s, &str)
-	core.Sink(str) // TODO(212) want "a source has reached a sink"
-}
-
-func colocateInnoc(core.Source, core.Innocuous) {}
-
-func TestNamedStructTypeIsNotTainted(s core.Source, i core.Innocuous) {
-	colocateInnoc(s, i)
-	core.Sink(i)
-}
-
-func colocateInnocPtr(core.Source, *core.Innocuous) {}
-
-func TestNamedStructPointerIsTainted(s core.Source, i *core.Innocuous) {
-	colocateInnocPtr(s, i)
-	core.Sink(i) // want "a source has reached a sink"
-}
-
-func TestPointerToNamedStructIsTainted(s core.Source, i core.Innocuous) {
-	// This test is failing because &x introduces an Alloc,
-	// and we don't traverse through non-array Allocs
-	colocateInnocPtr(s, &i)
-	core.Sink(i) // TODO(212) want "a source has reached a sink"
-}
-
-type PointerHolder struct{ ptr *core.Source }
-
-func colocatePointerHolder(core.Source, PointerHolder) {}
-
-func TestNamedStructPointerHolderIsTainted(s core.Source, ph PointerHolder) {
-	// This test is failing because ph is created by an Alloc,
-	// and we don't traverse through non-array Allocs
-	colocatePointerHolder(s, ph)
-	core.Sink(ph) // TODO(212) want "a source has reached a sink"
-}
-
-func colocateArrOfValues(core.Source, [1]string) {}
-
-func TestArrOfValuesIsNotTainted(s core.Source, arr [1]string) {
-	colocateArrOfValues(s, arr)
-	core.Sink(arr)
-}
-
-func colocateArrOfPointers(core.Source, [1]*string) {}
-
-func TestArrOfPointersIsTainted(s core.Source, arr [1]*string) {
-	colocateArrOfPointers(s, arr)
-	core.Sink(arr) // want "a source has reached a sink"
-}
-
-func colocateReferenceCollections(core.Source, map[string]string, chan string, []string) {}
-
-func TestReferenceCollectionsAreTainted(s core.Source) {
-	m := make(map[string]string)
-	c := make(chan string)
-	sl := make([]string, 0)
-	colocateReferenceCollections(s, m, c, sl)
-	core.Sink(m)  // want "a source has reached a sink"
-	core.Sink(c)  // want "a source has reached a sink"
-	core.Sink(sl) // want "a source has reached a sink"
-}
-
-func colocateReflectValue(core.Source, reflect.Value) {}
-
-func TestReflectValuesAreTainted(s core.Source, r reflect.Value) {
-	// This test is failing because r is created by an Alloc,
-	// and we don't traverse through non-array Allocs
-	colocateReflectValue(s, r)
-	core.Sink(r) // TODO(212) want "a source has reached a sink"
-}
-
-func colocateSingleInterface(s core.Source, e interface{})             {}
-func colocateVariadicInterface(s core.Source, taintees ...interface{}) {}
-
-func TestTaintedInterface(s core.Source, i interface{}) {
-	colocateSingleInterface(s, i)
-	core.Sink(i) // want "a source has reached a sink"
-
-}
-
-func TestTaintedThroughInterface(s core.Source, str string, i core.Innocuous) {
-	colocateVariadicInterface(s, str, i)
-	core.Sink(str)
-	core.Sink(i)
-}
-
-func TestPointerTaintedThroughInterface(s core.Source, str string, i core.Innocuous) {
-	colocateVariadicInterface(s, &str, &i)
-	core.Sink(str) // TODO(212) want "a source has reached a sink"
-	core.Sink(i)   // TODO(212) want "a source has reached a sink"
-}
-
 // CVE-2020-8564
 func TestTaintIsPropagatedToDataBeingUnmarshalled(contents []byte) (src core.Source, err error) {
 	if err = json.Unmarshal(contents, &src); err != nil {
@@ -141,18 +31,3 @@ func TestTaintIsPropagatedToDataBeingUnmarshalled(contents []byte) (src core.Sou
 	core.Sink(contents) // want "a source has reached a sink"
 	return
 }
-
-func colocateFunc(core.Source, func()) {}
-
-func TestTaintIsNotPropagatedToFunction(s core.Source) {
-	f := func() {}
-	// f is an *ssa.Function with type *types.Signature
-	colocateFunc(s, f)
-	core.Sink(f)
-}
-
-func TestTaintIsNotPropagatedToFunctionParameter(s core.Source, f func()) {
-	// f is an *ssa.Parameter with type *types.Signature
-	colocateFunc(s, f)
-	core.Sink(f)
-}
diff --git a/internal/pkg/levee/testdata/src/levee_analysistest/example/tests/extracts/tests.go b/internal/pkg/levee/testdata/src/levee_analysistest/example/tests/extracts/tests.go
index 8b6c926..6ece92c 100644
--- a/internal/pkg/levee/testdata/src/levee_analysistest/example/tests/extracts/tests.go
+++ b/internal/pkg/levee/testdata/src/levee_analysistest/example/tests/extracts/tests.go
@@ -30,10 +30,6 @@ func CreateSourceFlipped() (error, core.Source) {
 	return nil, core.Source{}
 }
 
-func TakeSource(s core.Source) (string, int, interface{}) {
-	return "", 0, nil
-}
-
 func TestOnlySourceExtractIsTaintedFromCall() {
 	s, ok := TryUpdateSource(core.Source{})
 	core.Sink(s) // want "a source has reached a sink"
@@ -77,13 +73,6 @@ func TestOnlySourceExtractIsTaintedInstructionOrderFlipped() {
 	core.Sink(s) // want "a source has reached a sink"
 }
 
-func TestExtractsFromCallWithSourceArgAreTainted(s core.Source) {
-	str, i, e := TakeSource(s)
-	core.Sink(str) // want "a source has reached a sink"
-	core.Sink(i)
-	core.Sink(e) // want "a source has reached a sink"
-}
-
 func NewSource() (*core.Source, error) {
 	return &core.Source{}, nil
 }
diff --git a/internal/pkg/levee/testdata/src/levee_analysistest/example/tests/makeinterface/tests.go b/internal/pkg/levee/testdata/src/levee_analysistest/example/tests/makeinterface/tests.go
deleted file mode 100644
index 0c543fc..0000000
--- a/internal/pkg/levee/testdata/src/levee_analysistest/example/tests/makeinterface/tests.go
+++ /dev/null
@@ -1,28 +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 deletethis
-
-import (
-	"levee_analysistest/example/core"
-)
-
-func TestTaintingInterfaceValueDoesNotTaintContainedValue(s *core.Source, str string) {
-	var x interface{} = str
-	colocate(s, x)
-	// TODO(#209): no report should be produced below
-	core.Sink(str) // want "a source has reached a sink"
-}
-
-func colocate(s *core.Source, x interface{}) {}
diff --git a/internal/pkg/levee/testdata/src/levee_analysistest/example/tests/method/tests.go b/internal/pkg/levee/testdata/src/levee_analysistest/example/tests/method/tests.go
deleted file mode 100644
index 85ef99b..0000000
--- a/internal/pkg/levee/testdata/src/levee_analysistest/example/tests/method/tests.go
+++ /dev/null
@@ -1,29 +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 method
-
-import (
-	"levee_analysistest/example/core"
-)
-
-func TestMethodCallOnStaticallyKnownReceiverPropagatesTaint(s core.Source) {
-	data := s.Propagate(s.Data)
-	core.Sink(data) // want "a source has reached a sink"
-}
-
-func TestMethodCallOnStaticallyUnknownReceiverPropagatesTaint(sm core.SourceManipulator, s core.Source) {
-	data := sm.Propagate(s.Data)
-	core.Sink(data) // want "a source has reached a sink"
-}
diff --git a/internal/pkg/levee/testdata/src/levee_analysistest/example/tests/propagation/functions.go b/internal/pkg/levee/testdata/src/levee_analysistest/example/tests/propagation/functions.go
index dfbf959..e6c53a7 100644
--- a/internal/pkg/levee/testdata/src/levee_analysistest/example/tests/propagation/functions.go
+++ b/internal/pkg/levee/testdata/src/levee_analysistest/example/tests/propagation/functions.go
@@ -15,41 +15,13 @@
 package propagation
 
 import (
-	"fmt"
-
+	"errors"
 	"levee_analysistest/example/core"
+	"os"
 )
 
-func Identity(arg interface{}) interface{} {
-	return arg
-}
-
-func TestIdentityPropagator(s core.Source) {
-	i := Identity(s)
-	core.Sink(i)           // want "a source has reached a sink"
-	core.Sink(Identity(s)) // want "a source has reached a sink"
-}
-
-func ToString(arg interface{}) string {
-	return fmt.Sprintf("%v", arg)
-}
-
-func TestToStringPropagator(s core.Source) {
-	v := ToString(s)
-	core.Sink(v) // want "a source has reached a sink"
-}
-
-func TestPropagationViaSourceMethod(s core.Source) {
-	tainted := s.Propagate(s.Data)
-	core.Sink(tainted) // want "a source has reached a sink"
-}
-
-func TestPropagationViaFunctionReturningBool(s *core.Source, i *core.Innocuous) {
-	if ok := TryCopy(i, s); !ok {
-		core.Sinkf("couldn't copy to: %v", i) // want "a source has reached a sink"
+func TestPropagationViaFunctionReturningBool(s core.Source, err *os.PathError) {
+	if ok := errors.As(errors.New(s.Data), err); !ok {
+		core.Sinkf("not a PathError: %v", err) // want "a source has reached a sink"
 	}
 }
-
-func TryCopy(dst interface{}, src interface{}) bool {
-	return true
-}
diff --git a/internal/pkg/levee/testdata/src/levee_analysistest/example/tests/stdlib/funcs.go b/internal/pkg/levee/testdata/src/levee_analysistest/example/tests/stdlib/funcs.go
new file mode 100644
index 0000000..5a27d89
--- /dev/null
+++ b/internal/pkg/levee/testdata/src/levee_analysistest/example/tests/stdlib/funcs.go
@@ -0,0 +1,99 @@
+// 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 stdlib
+
+import (
+	"bufio"
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io"
+	"levee_analysistest/example/core"
+	"strings"
+	"sync"
+	"text/template"
+)
+
+func TestTaintFromArgumentToReturnValue(s core.Source) {
+	core.Sink(fmt.Errorf(s.Data))         // want "a source has reached a sink"
+	core.Sink(strings.Split(s.Data, ",")) // want "a source has reached a sink"
+}
+
+func TestTaintFromArgumentToArgument(w io.Writer, s core.Source) {
+	// w hasn't been tainted yet
+	core.Sink(w)
+
+	fmt.Fprintf(w, s.Data)
+	core.Sink(w) // want "a source has reached a sink"
+}
+
+func TestTaintFromFourthArgumentToSecondArgument(t *template.Template, w io.Writer, s core.Source) {
+	err := t.ExecuteTemplate(w, "template", s.Data)
+	core.Sink(w) // want "a source has reached a sink"
+	core.Sink(err)
+}
+
+func TestTaintFromFirstArgumentToReceiver(m *sync.Map, s core.Source) {
+	m.Store(s.Data, nil)
+	core.Sink(m) // want "a source has reached a sink"
+}
+
+func TestTaintFromSecondArgumentToReceiver(m *sync.Map, s core.Source) {
+	m.Store(nil, s.Data)
+	core.Sink(m) // want "a source has reached a sink"
+}
+
+func TestTaintFromArgumentToReceiver(scan bufio.Scanner, src core.Source) {
+	scan.Buffer([]byte(src.Data), 1024)
+	core.Sink(scan)        // TODO(#212) want "a source has reached a sink"
+	core.Sink(scan.Text()) // TODO(#212) want "a source has reached a sink"
+}
+
+func TestTaintFromArgumentToPtrReceiver(scan *bufio.Scanner, src core.Source) {
+	scan.Buffer([]byte(src.Data), 1024)
+	core.Sink(scan)        // want "a source has reached a sink"
+	core.Sink(scan.Text()) // want "a source has reached a sink"
+}
+
+func TestTaintFromReceiverToReturnValue(s core.Source) {
+	b := bytes.NewBufferString(s.Data)
+	line, err := b.ReadString(' ')
+	core.Sink(line) // want "a source has reached a sink"
+	core.Sink(err)
+}
+
+func TestTaintFromReceiverToArgument(str *string, src core.Source) {
+	dec := json.NewDecoder(strings.NewReader(src.Data))
+	dec.Decode(str)
+	core.Sink(str) // want "a source has reached a sink"
+}
+
+func TestTaintFromVariadicToReturnValue(s core.Source) {
+	core.Sink(fmt.Errorf("here's some data: %v", s.Data)) // want "a source has reached a sink"
+}
+
+func TestTaintFromMultiArgumentVariadicToReturnValue(s core.Source) {
+	core.Sink(fmt.Errorf("source %d has data: %v", s.ID, s.Data)) // want "a source has reached a sink"
+}
+
+func TestTaintFromVariadicToArgument(w io.Writer, s core.Source) {
+	fmt.Fprint(w, s.Data)
+	core.Sink(w) // want "a source has reached a sink"
+}
+
+func TestTaintVariadic(str *string, src core.Source) {
+	fmt.Sscan(src.Data, &str)
+	core.Sink(str) // TODO(#291) want "a source has reached a sink"
+}
diff --git a/internal/pkg/levee/testdata/src/levee_analysistest/example/tests/stdlib/interface_funcs.go b/internal/pkg/levee/testdata/src/levee_analysistest/example/tests/stdlib/interface_funcs.go
new file mode 100644
index 0000000..291873f
--- /dev/null
+++ b/internal/pkg/levee/testdata/src/levee_analysistest/example/tests/stdlib/interface_funcs.go
@@ -0,0 +1,46 @@
+// 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 stdlib
+
+import (
+	"context"
+	"io"
+	"levee_analysistest/example/core"
+	"strings"
+)
+
+func TestPropagateToAndFromConcreteReceiver(b *strings.Builder, s core.Source) {
+	b.WriteString(s.Data)
+	core.Sink(b)          // want "a source has reached a sink"
+	core.Sink(b.String()) // want "a source has reached a sink"
+}
+
+func TestPropagateToAndFromAbstractReceiver(w io.ReadWriter, b []byte, s core.Source) {
+	w.Write([]byte(s.Data))
+	w.Read(b)
+	core.Sink(w) // want "a source has reached a sink"
+	core.Sink(b) // want "a source has reached a sink"
+}
+
+func TestPropagationInvolvingFuncWithInterfaceParameter(rf io.ReaderFrom, s core.Source) {
+	rf.ReadFrom(strings.NewReader(s.Data))
+	core.Sink(rf) // want "a source has reached a sink"
+}
+
+func TestPropagateThroughContext(c context.Context, s core.Source) {
+	cc := context.WithValue(c, "data", s.Data)
+	core.Sink(cc.Err())         // want "a source has reached a sink"
+	core.Sink(cc.Value("data")) // want "a source has reached a sink"
+}
diff --git a/internal/pkg/propagation/summary/api_test.go b/internal/pkg/propagation/summary/api_test.go
new file mode 100644
index 0000000..d68c1c8
--- /dev/null
+++ b/internal/pkg/propagation/summary/api_test.go
@@ -0,0 +1,75 @@
+// 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 summary
+
+import (
+	"go/types"
+	"testing"
+
+	"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"
+)
+
+func Test(t *testing.T) {
+	analysistest.Run(t, analysistest.TestData(), analyzer, "./...")
+}
+
+var analyzer = &analysis.Analyzer{
+	Name:     "summarytest",
+	Doc:      "This analyzer is a test harness for functions in the summary package.",
+	Run:      run,
+	Requires: []*analysis.Analyzer{buildssa.Analyzer},
+}
+
+func run(pass *analysis.Pass) (interface{}, error) {
+	builtSSA := pass.ResultOf[buildssa.Analyzer].(*buildssa.SSA)
+
+	// produce reports for sigTypeString test cases
+	for _, f := range builtSSA.SrcFuncs {
+		if f.Name() == "testStaticFuncName" || f.Name() == "testFuncNameWithoutReceiver" {
+			continue
+		}
+		sig := f.Type().(*types.Signature)
+		pass.Reportf(f.Pos(), sigTypeString(sig))
+	}
+
+	// produce reports for testFuncName test cases
+	for _, f := range builtSSA.SrcFuncs {
+		if f.Name() != "testStaticFuncName" {
+			continue
+		}
+		for _, instr := range f.Blocks[0].Instrs {
+			if call, ok := instr.(*ssa.Call); ok {
+				pass.Reportf(call.Pos(), staticFuncName(call))
+			}
+		}
+	}
+
+	// produce reports for testFuncNameWithoutReceiver test cases
+	for _, f := range builtSSA.SrcFuncs {
+		if f.Name() != "testFuncNameWithoutReceiver" {
+			continue
+		}
+		for _, instr := range f.Blocks[0].Instrs {
+			if call, ok := instr.(*ssa.Call); ok {
+				pass.Reportf(call.Pos(), methodNameWithoutReceiver(call))
+			}
+		}
+	}
+
+	return nil, nil
+}
diff --git a/internal/pkg/propagation/summary/testdata/src/summary_analysistest/go.mod b/internal/pkg/propagation/summary/testdata/src/summary_analysistest/go.mod
new file mode 100644
index 0000000..104608e
--- /dev/null
+++ b/internal/pkg/propagation/summary/testdata/src/summary_analysistest/go.mod
@@ -0,0 +1,3 @@
+module summary_analysistest
+
+go 1.15
diff --git a/internal/pkg/propagation/summary/testdata/src/summary_analysistest/tests/test.go b/internal/pkg/propagation/summary/testdata/src/summary_analysistest/tests/test.go
new file mode 100644
index 0000000..85fa8ac
--- /dev/null
+++ b/internal/pkg/propagation/summary/testdata/src/summary_analysistest/tests/test.go
@@ -0,0 +1,55 @@
+// 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 test
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io"
+)
+
+func testStaticFuncName(w io.Writer) {
+	// positive cases
+	var b bytes.Buffer
+	b.Write(nil)            // want `^\Q(*bytes.Buffer).Write\E$`
+	fmt.Println()           // want "^fmt.Println$"
+	json.Unmarshal(nil, &b) // want "^encoding/json.Unmarshal$"
+
+	// negative cases
+	w.Write(nil) // want "^$"
+	println()    // want "^$"
+}
+
+func testFuncNameWithoutReceiver(w io.Writer) {
+	// positive cases
+	var b bytes.Buffer
+	b.Write(nil) // want "^Write$"
+	w.Write(nil) // want "^Write$"
+
+	// negative cases
+	fmt.Println() // want "^$"
+	println()     // want "^$"
+}
+
+func testFuncSignature(slice *[]*interface{}, m map[foo]string, r io.Reader) (err error, oops bool) { // want `^\Q(*[]*interface{},map[foo]string,Reader)(error,bool)\E$`
+	return nil, false
+}
+
+func (f *foo) testMethodSignature(i int, d float64) *foo { // want `^\Q(int,float64)(*foo)\E$`
+	return f
+}
+
+type foo struct{}