Repository: google/go-flow-levee
Pull request #285 (2021-02-25 07:32:06)

Ear Pointer Analysis
…ter analysis based on equivalent abstract references (which is useful for may-alias relationships).

This is the first step of introducing an inter-procedural, field sensitive, and context sensitive analysis for Go.

Fixes #<issue_number_goes_here>

> It's a good idea to open an issue first for discussion.

- [ ] 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.)
- [ ] Appropriate changes to README are included in PR


mlevesquedion: Hi @guodongli-google,

Thank you for this PR! :tada: 

As I understand it, this introduces the core data structures for an `analysis` pass which will be added in a future PR? (Judging from this removed file: https://github.com/google/go-flow-levee/pull/285/commits/edbd83811d1137bdc9524abd2c8cb797182e1ec7)

I will take a deeper look later today.

In the meantime, could you fix the failing `lint` in CI by running `goimports -w internal/pkg/EAR/ear_state_test.go internal/pkg/EAR/heap.go`?
mlevesquedion: Our CI stops on the first error, but once `goimports` is fixed it will complain about a few other things:

Licenses are missing for the following files:

`internal/pkg/EAR/ear_state_test.go`
`internal/pkg/EAR/heap.go`

Also, although our linter check won't complain about it, this file doesn't have the full license:
`internal/pkg/EAR/ear_state.go`

Please add the full license to these files:
```
// Copyright 2021 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.
```

Staticcheck is complaining about some unused code:
```
internal/pkg/EAR/heap.go:82:2: field irField is unused (U1000)
internal/pkg/EAR/heap.go:96:6: func getIndexField is unused (U1000)
internal/pkg/EAR/heap.go:118:6: func getAbsReferenceForInternal is unused (U1000)
internal/pkg/EAR/heap.go:126:6: func typeMayShareObject is unused (U1000)
```

If you plan to use this code in a future PR, you may suppress the reports with a comment on the lines above where each report is produced:
```
//lint:ignore U1000 ignore dead code for now
```

Patch:
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index b0659ee..49f6f4a 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -27,6 +27,7 @@ jobs:
         uses: actions/checkout@v1
 
       - name: Check goimports
+        if: ${{ always() }}
         run: |
           go install golang.org/x/tools/cmd/goimports
           if [[ $(goimports -l cmd internal pkg) ]]; then
@@ -35,6 +36,7 @@ jobs:
           fi
 
       - name: Check go mod
+        if: ${{ always() }}
         run: |
           go mod tidy
           if ! git diff --quiet; then
@@ -43,16 +45,19 @@ jobs:
           fi
 
       - name: Check for license headers
+        if: ${{ always() }}
         run: |
           go get -u github.com/google/addlicense
           addlicense -check cmd internal pkg
 
       - name: Run staticcheck
+        if: ${{ always() }}
         run: |
           go install honnef.co/go/tools/cmd/staticcheck
           staticcheck ./...
 
       - name: Check YAML
+        if: ${{ always() }}
         run: |
           yamllint .
 
diff --git a/README.md b/README.md
index d07695b..41a060d 100644
--- a/README.md
+++ b/README.md
@@ -44,7 +44,7 @@ Static taint propagation analysis is a hard problem. In fact, it is [undecidable
 * False negatives: the analyzer may fail to recognize that a piece of code is unsafe.
 * False positives: the analyzer may incorrectly claim that a safe piece of code is unsafe. 
 
-Since taint propagation is often used as a security safeguard, we care more deeply about false negatives. If you discover unsafe code that the analyzer is not recognizing as unsafe, please open an issue [here](https://github.com/google/go-flow-levee/issues/new?template=false-negative.md). Conversely, fales positives waste developer time and should also be addressed. If the analyzer produces a report for code that you consider to be safe, please open an issue [here](https://github.com/google/go-flow-levee/issues/new?template=false-positive.md).
+Since taint propagation is often used as a security safeguard, we care more deeply about false negatives. If you discover unsafe code that the analyzer is not recognizing as unsafe, please open an issue [here](https://github.com/google/go-flow-levee/issues/new?template=false-negative.md). Conversely, false positives waste developer time and should also be addressed. If the analyzer produces a report for code that you consider to be safe, please open an issue [here](https://github.com/google/go-flow-levee/issues/new?template=false-positive.md).
 
 For general bug reports (e.g. crashes), please open an issue [here](https://github.com/google/go-flow-levee/issues/new?template=bug_report.md).
 
diff --git a/internal/pkg/config/config.go b/internal/pkg/config/config.go
index f5c8360..d72b8af 100644
--- a/internal/pkg/config/config.go
+++ b/internal/pkg/config/config.go
@@ -33,12 +33,24 @@ var (
 	// FlagSet should be used by analyzers to reuse -config flag.
 	FlagSet    flag.FlagSet
 	configFile string
+
+	configBytes        []byte
+	configBytesOnce    sync.Once
+	configFromBytes    *Config
+	configFromBytesErr error
 )
 
 func init() {
 	FlagSet.StringVar(&configFile, "config", "config.yaml", "path to analysis configuration file")
 }
 
+// SetBytes allows the contents of a configuration file
+// to be provided directly. This is useful when running the tool
+// in an environment where file access is inconvenient.
+func SetBytes(b []byte) {
+	configBytes = b
+}
+
 // Config contains matchers and analysis scope information.
 type Config struct {
 	ReportMessage             string
@@ -295,12 +307,29 @@ func (fm funcMatcher) MatchFunction(path, receiver, name string) bool {
 	return fm.Package.MatchString(path) && fm.Receiver.MatchString(receiver) && fm.Method.MatchString(name)
 }
 
-// ReadConfig fetches configuration from the config cache.
-// The cache reads, parses, and validates config file if necessary.
+// ReadConfig reads configuration from the config cache.
+// The cache reads, parses, and validates the config file if necessary.
+// If the config bytes were set using SetConfigBytes, they are used instead.
 func ReadConfig() (*Config, error) {
+	if configBytes != nil {
+		return readConfigBytes()
+	}
+
 	return cache.read(configFile)
 }
 
+func readConfigBytes() (*Config, error) {
+	configBytesOnce.Do(func() {
+		configFromBytes = new(Config)
+		configFromBytesErr = yaml.UnmarshalStrict(configBytes, configFromBytes)
+		if configFromBytesErr != nil {
+			fmt.Println(configFromBytesErr)
+		}
+	})
+
+	return configFromBytes, configFromBytesErr
+}
+
 // configCacheElement reduces disk access across multiple ReadConfig calls.
 type configCacheElement struct {
 	once       sync.Once
@@ -314,11 +343,13 @@ func (r *configCacheElement) readOnce() (*Config, error) {
 		c := new(Config)
 		bytes, err := ioutil.ReadFile(r.sourceFile)
 		if err != nil {
+			fmt.Println(err)
 			r.err = fmt.Errorf("error reading analysis config: %v", err)
 			return
 		}
 
 		if err := yaml.UnmarshalStrict(bytes, c); err != nil {
+			fmt.Println(err)
 			r.err = err
 			return
 		}
diff --git a/internal/pkg/earpointer/heap.go b/internal/pkg/earpointer/heap.go
new file mode 100644
index 0000000..ac1a72f
--- /dev/null
+++ b/internal/pkg/earpointer/heap.go
@@ -0,0 +1,211 @@
+// Copyright 2021 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 earpointer
+
+import (
+	"fmt"
+	"go/types"
+	"strings"
+
+	"golang.org/x/tools/go/ssa"
+)
+
+// An abstract reference is a local variable (register), global variable,
+// or a synthetic element, possibly annotated with some kind of context
+// (call stack, allocation site etc).
+
+// Reference is the base interface for local, global, and synthetic references.
+type Reference interface {
+	fmt.Stringer
+
+	Type() types.Type // Return the data type of the underlying element
+	Value() ssa.Value // Return the underlying local or global
+}
+
+// Context represents the calling context of a reference.
+// Typically, it contains a stack of call instructions.
+type Context []ssa.Instruction
+
+// Local is a heap partition that represents all abstract
+// objects that a register of reference type can point to in a specific context.
+type Local struct {
+	context *Context
+	reg     ssa.Value
+}
+
+func (l Local) Type() types.Type {
+	return l.reg.Type()
+}
+
+func (l Local) Value() ssa.Value {
+	return l.reg
+}
+
+func (l Local) String() string {
+	reg := l.reg
+	if p := reg.Parent(); p != nil {
+		return l.context.String() + p.Name() + "." + reg.Name()
+	}
+	return l.context.String() + reg.Name()
+}
+
+// Global is a heap partition that represents all abstract
+// objects that a global of reference type can point to.
+type Global struct {
+	global *ssa.Global
+}
+
+func (g Global) Type() types.Type {
+	return g.global.Type()
+}
+
+func (g Global) Value() ssa.Value {
+	return g.global
+}
+
+func (g Global) String() string {
+	return g.global.String()
+}
+
+type SyntheticKind int
+
+const (
+	SyntheticValueOf SyntheticKind = iota
+	SyntheticField
+)
+
+// Synthetic is for references created internally with an operator
+// and an operand reference. It is for creating references not directly associated
+// with an IR element. For example,
+// (1) *r0 can be represented by using operator SyntheticValueOf (*);
+// (2) a unknown field can be represented by using operator SyntheticField.
+//     For example, when a struct is cloned, some fields may not appear in the IR,
+//     these "unused" fields can be synthesized and stored in the heap.
+type Synthetic struct {
+	kind SyntheticKind
+	ref  Reference
+}
+
+func (s Synthetic) Type() types.Type {
+	return s.ref.Type()
+}
+
+func (s Synthetic) Value() ssa.Value {
+	return s.ref.Value()
+}
+
+func (s Synthetic) String() string {
+	if s.kind == SyntheticValueOf {
+		return "*" + s.ref.String()
+	}
+	return s.ref.String() + "[.]"
+}
+
+// Field can be (1) a struct field linked to an IR field (Var);
+// (2) or one with a name but not linked to any IR element (irField = nil).
+type Field struct {
+	Name string
+	//lint:ignore U1000 ignore dead code for now
+	irField *types.Var
+}
+
+// Helper to get the pseudo-field that is used to denote looking up an
+// array/slice/map index. For example, consider a register x of type T[], where
+// T is a struct type. The contents of the array/slice, namely x[i], is
+// over-approximated as the heap partition pointed-to by a pseudo-field named
+// anyIndexField.
+//lint:ignore U1000 ignore dead code for now
+var anyIndexField Field = Field{Name: "AnyField"}
+
+// A pseudo-field to denote the direct points-to relation. For example, r1 = &r0
+// is modeled as r1[directPointToField] = r0.
+var directPointToField Field = Field{Name: "->"}
+
+// Commonly used data structures.
+
+// ReferenceSet is a hash set of references.
+type ReferenceSet map[Reference]bool
+
+// FieldMap maps from Fields to a reference handle.
+// It is used for maintaining points-to relationships for the fields of a
+// reference partition. The number of fields is not expected to
+// be high on average, so a tree map may be used instead to save memory.
+type FieldMap map[Field]Reference
+
+// Constructs Reference for local "reg" in context "context".
+// TODO: hashconst the object to save memory.
+func MakeLocal(context *Context, reg ssa.Value) Local {
+	return Local{context: context, reg: reg}
+}
+
+func MakeLocalWithEmptyContext(reg ssa.Value) Local {
+	return Local{context: &emptyContext, reg: reg}
+}
+
+func MakeGlobal(global *ssa.Global) Global {
+	return Global{global: global}
+}
+
+func MakeReference(context *Context, reg ssa.Value) Reference {
+	if global, ok := reg.(*ssa.Global); ok {
+		return Global{global: global}
+	}
+	return Local{context: context, reg: reg}
+}
+
+// Constructs synthetic reference.
+func MakeSynthetic(kind SyntheticKind, ref Reference) Synthetic {
+	return Synthetic{kind: kind, ref: ref}
+}
+
+// Returns whether a value of this type may share an object pointed by other
+// values. It is used for identifying copy-by-reference objects. For example, it
+// returns false for any integer type, and returns true for pointer type and
+// struct type.
+//lint:ignore U1000 ignore dead code for now
+func typeMayShareObject(tp types.Type) bool {
+	switch tp := tp.(type) {
+	case *types.Pointer,
+		*types.Struct,
+		*types.Chan,
+		*types.Interface,
+		*types.Slice,
+		*types.Signature,
+		*types.Array,
+		*types.Map:
+		return true
+	case *types.Basic:
+		if tp.Kind() == types.String || tp.Kind() == types.UnsafePointer {
+			return true
+		}
+	case *types.Named:
+		return typeMayShareObject(tp.Underlying())
+	}
+	return false
+}
+
+// emptyContext defines an empty context handle to be shared by local references.
+var emptyContext Context
+
+func (ctx Context) String() string {
+	if len(ctx) == 0 {
+		return ""
+	}
+	s := make([]string, len(ctx))
+	for i, ins := range ctx {
+		s[i] = ins.String()
+	}
+	return "[" + strings.Join(s, "; ") + "]"
+}
diff --git a/internal/pkg/earpointer/state.go b/internal/pkg/earpointer/state.go
new file mode 100644
index 0000000..2292add
--- /dev/null
+++ b/internal/pkg/earpointer/state.go
@@ -0,0 +1,422 @@
+// Copyright 2021 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 earpointer introduces a pointer analysis based on unifying
+// equivalent abstract references (EAR), which implements Steensgaard's algorithm.
+// Extensions include adding context sensitivity and field sensitivity.
+// Abstract references defines the data structure to represent abstract heap.
+package earpointer
+
+import (
+	"log"
+	"sort"
+	"strings"
+)
+
+// parentMap maps a reference to its representative (i.e. the parent
+// in the union-find tree).
+type parentMap map[Reference]Reference
+
+// partitionInfo maintains information at the partition level (which
+// consists of a set of references). It maintains the size of the
+// partition (used for weighted join in union-find) and a FieldMap,
+// which is a mapping from values F (instance fields, array contents etc) to
+// references R.
+type partitionInfo struct {
+	numMembers uint     // number of members
+	fieldRefs  FieldMap // field mapping
+}
+
+// partitionInfoMap maps a reference to its partition.
+type partitionInfoMap map[Reference]partitionInfo
+
+// state is Equivalent Abstract references (EAR) state, which maintains
+// an abstract heap information. It contains data structures that
+// may be mutated during heap construction.
+type state struct {
+	// Map from a ref (a partition representative) to information about
+	// this partition.
+	partitions partitionInfoMap
+	// Map from a ref to its parent abstract ref.
+	parents parentMap
+}
+
+// NewState creates an empty abstract state.
+func NewState() *state {
+	return &state{
+		partitions: make(partitionInfoMap),
+		parents:    make(parentMap),
+	}
+}
+
+// references gets all references.
+func (state *state) references() ReferenceSet {
+	refs := make(ReferenceSet)
+	for k := range state.parents {
+		refs[k] = true
+	}
+	return refs
+}
+
+// representatives gets all references that are partition representatives.
+//lint:ignore U1000 ignore dead code for now
+func (state *state) representatives() ReferenceSet {
+	reps := make(ReferenceSet)
+	for k, v := range state.parents {
+		if k == v {
+			reps[k] = true
+		}
+	}
+	return reps
+}
+
+// representative gets the partition representative of reference "ref"
+// ("ref" must belong to this state).
+func (state *state) representative(ref Reference) Reference {
+	failureCallback := func(ref Reference) Reference {
+		log.Fatalf("representative: Reference [%s] not found in state", ref)
+		return nil
+	}
+	return state.lookupPartitionRep(ref, failureCallback)
+}
+
+// PartitionFieldMap gets the partition-level field map for partition
+// representative "ref". The result maps a field to a partition representative.
+// Note "ref" must be a partition representative.
+func (state *state) PartitionFieldMap(ref Reference) FieldMap {
+	return state.partitionInfo(ref).fieldRefs
+}
+
+// Pretty printer
+func (state *state) String() string {
+	refs := state.references()
+	members := make(map[Reference][]Reference)
+	for ref := range refs {
+		rep := state.representative(ref)
+		members[rep] = append(members[rep], ref)
+	}
+
+	var pstrs []string
+	for rep, mems := range members {
+		mstrs := make([]string, len(mems))
+		for i, v := range mems {
+			mstrs[i] = v.String()
+		}
+		sort.Strings(mstrs)
+		pstr := "{" + strings.Join(mstrs, ",") + "}: " +
+			state.PartitionFieldMap(rep).String()
+		pstrs = append(pstrs, pstr)
+	}
+	sort.Strings(pstrs)
+	return strings.Join(pstrs, ", ")
+}
+
+// Pretty prints a field map.
+func (fmap FieldMap) String() string {
+	if v, ok := fmap[directPointToField]; ok {
+		return "--> " + v.String()
+	}
+	var fstrs []string
+	for k, v := range fmap {
+		fstrs = append(fstrs, k.Name+"->"+v.String())
+	}
+	sort.Strings(fstrs)
+	return "[" + strings.Join(fstrs, ", ") + "]"
+}
+
+// Various state mutation operation
+
+// Insert inserts reference "ref" to the state and returns the current
+// partition representative of "ref".
+func (state *state) Insert(ref Reference) Reference {
+	// Lookup failure will create new entry in the parent table.
+	failureCallback := func(ref Reference) Reference {
+		state.parents[ref] = ref
+		state.partitions[ref] = partitionInfo{numMembers: 1, fieldRefs: make(FieldMap)}
+		return ref
+	}
+	return state.lookupPartitionRep(ref, failureCallback)
+}
+
+// Unify unifies the references "ref1" and "ref2"
+func (state *state) Unify(ref1 Reference, ref2 Reference) {
+	state.unifyReps(state.representative(ref1), state.representative(ref2))
+}
+
+// unifyReps unifies the references "ref1" and "ref2", assuming both are
+// partition representative. This allows to avoid calling representative
+// unnecesarilly. Using this function with non-representative
+// references will raise an error.
+func (state *state) unifyReps(ref1 Reference, ref2 Reference) {
+	// If they are in the same partition already, we are done.
+	if ref1 == ref2 {
+		return
+	}
+	// Create local copies to use swap (this improves the readability).
+	prep1, prep2 := ref1, ref2
+	// Find partition-level info for "ref1" and "ref2"
+	pinfo1 := state.partitionInfo(prep1)
+	pinfo2 := state.partitionInfo(prep2)
+	// swap so that "ref1" is always the smaller partition
+	if pinfo1.numMembers > pinfo2.numMembers {
+		prep1, prep2 = prep2, prep1
+		pinfo1, pinfo2 = pinfo2, pinfo1
+	}
+
+	// Create state by having "prep1" point to "prep2" as parent and
+	// by erasing "prep1" as a partition rep. We then call MergeFieldMap()
+	// to merge field maps (which can trigger further unification). This process
+	// will converge since the number of partitions is guaranteed to decrease at
+	// every unification and is lower-bounded by 1.
+
+	// Update parent pointer and partition info map for ref1 while retaining
+	// its field map in fmap1.
+	state.parents[prep1] = prep2
+	fmap1 := pinfo1.fieldRefs
+	pinfo1.fieldRefs = nil
+	delete(state.partitions, prep1)
+
+	// Update partition info for ref2 and merge the field map fmap1 in-place.
+	pinfo2.numMembers += pinfo1.numMembers
+	state.mergeFieldMap(fmap1, pinfo2.fieldRefs)
+}
+
+// Helper to find a partition representative. It calls onfailure callback
+// to delegate the action on lookup failure.
+func (state *state) lookupPartitionRep(ref Reference, onfailure func(abs Reference) Reference) Reference {
+	rep, ok := state.parents[ref]
+	if !ok {
+		res := onfailure(ref)
+		return res
+	}
+	// If we found the partition rep, return right away.
+	if rep == ref {
+		return rep
+	}
+	// Else recurse. We use crashing callback here, because when recursing
+	// we expect the recursion argument to be always in the abstract state.
+	failureCallback := func(ref Reference) Reference {
+		log.Printf("lookupPartitionRep: Reference [%s] not found in state", ref)
+		return nil
+	}
+	prep := state.lookupPartitionRep(rep, failureCallback)
+	// Perform path compression before returning to caller.
+	state.parents[ref] = prep
+	return prep
+}
+
+// mergeFieldMap sets the partition info for "rep" as a merge of the field
+// maps in "oldMap" and "newMap" (the last is an out-parameter to save
+// on memory copies).
+func (state *state) mergeFieldMap(oldMap FieldMap, newMap FieldMap) {
+	// Merge field maps of "oldMap" and update "newMap" suitably. For any
+	// common fields, we unify the pointers, else we copy over the field mapping
+	// as such.  We first compute the list of references to unify, update the
+	// field map, and then perform unification.
+	toUnify := make(map[Reference]Reference)
+	// Merging old fmap to a new fmap. We iterate over oldMap and insert all elements
+	// to newMap. If a field already exists in the newMap, we unify the corresponding heap
+	// partitions. This takes linear time.
+	for k, v := range oldMap {
+		w, ok := newMap[k]
+		if !ok {
+			newMap[k] = v
+		} else {
+			toUnify[v] = w
+		}
+	}
+	// unify all necessary field partitions.
+	for k, v := range toUnify {
+		state.Unify(k, v)
+	}
+}
+
+// Helper to obtain the partition info for "ref" (which should be a partition
+// representative)
+func (state *state) partitionInfo(ref Reference) *partitionInfo {
+	v, ok := state.partitions[ref]
+	if !ok {
+		log.Fatalf("Reference [%s] is not a partition rep", ref)
+	}
+	return &v
+}
+
+// valueReferenceOrNil returns the reference holding the value *r of an address r
+// which is a partition representative.
+// Return nil if such a value reference doesn't exists.
+//lint:ignore U1000 ignore dead code for now
+func (state *state) valueReferenceOrNil(addr Reference) Reference {
+	// In the heap, r --> *r is implemented as r[directPointToField] = *r.
+	fmap := state.PartitionFieldMap(addr)
+	if v, ok := fmap[directPointToField]; ok {
+		return v
+	}
+	return nil
+}
+
+// Partitions represents the state with the partitions fully constructed, after which
+// no more mutation operations will be performed. Its internal data structures are
+// optimized for lookups only.
+type Partitions struct {
+	// Inherit "parents" and "partitions" from state.
+
+	// Map from a ref to its parent abstract ref.
+	parents parentMap
+	// Map from a ref (a partition representative) to information about
+	// this partition, esp. field maps.
+	partitions partitionInfoMap
+
+	// Extra data structures for partitions.
+
+	// Mapping from a partition representative to its members. This map is
+	// initialize in finalize method, which guarantees to avoid duplication.
+	// Users are accessing this information via PartitionMembers method,
+	// which does not allow any modification.
+	members map[Reference][]Reference
+
+	// Map from each field reference to its parent references. For example, record
+	// r->o for "o.x = r". This map is for accelerating external queries on
+	// fields, and is not required by the EAR analysis itself.
+	// It is constructed separately using the "ConstructFieldParentMap()"
+	// at the final phase.
+	revFields map[Reference][]Reference
+}
+
+func (state *state) ToPartitions() *Partitions {
+	p := &Partitions{
+		parents:    state.parents,
+		partitions: state.partitions,
+		members:    make(map[Reference][]Reference),
+		revFields:  make(map[Reference][]Reference),
+	}
+	p.finalize()
+	return p
+}
+
+// finalize indicates that no more mutation operations will be performed.
+// After this call, internal data structures are optimized for lookups only,
+// and no mutations should be made.
+func (p *Partitions) finalize() {
+	// Construct the map from field references to their parents.
+	p.constructFieldParentMap()
+
+	// Construct the members. members maps partitions representatives to the
+	// members (references) of partitions.
+	for ref, parent := range p.parents {
+		if ref == parent {
+			// If ref is equal to its parent, then ref is partition
+			// representative. No need to call representative.
+			p.members[ref] = append(p.members[ref], ref)
+		} else {
+			rep := p.Representative(ref)
+			// Otherwise, get ref's partition representative and update members.
+			p.members[rep] = append(p.members[rep], ref)
+		}
+	}
+	// Normalize fieldRefs table in a way that it only contains partition
+	// representatives.
+	for _, pinfo := range p.partitions {
+		for fd, entry := range pinfo.fieldRefs {
+			pinfo.fieldRefs[fd] = p.Representative(entry)
+		}
+	}
+}
+
+// Has returns true if "ref" is a reference present in the state.
+func (p *Partitions) Has(ref Reference) bool {
+	_, ok := p.parents[ref]
+	return ok
+}
+
+// References gets all references.
+func (p *Partitions) References() ReferenceSet {
+	refs := make(ReferenceSet)
+	for k := range p.parents {
+		refs[k] = true
+	}
+	return refs
+}
+
+// Representatives gets all references that are partition representatives.
+func (p *Partitions) Representatives() ReferenceSet {
+	reps := make(ReferenceSet)
+	for k := range p.members {
+		reps[k] = true
+	}
+	return reps
+}
+
+// Representative gets the partition representative of reference "ref"
+// ("ref" must belong to this state).
+func (p *Partitions) Representative(ref Reference) Reference {
+	return p.parents[ref]
+}
+
+// PartitionFieldMap gets the partition-level field map for partition
+// representative "ref". The result maps a field (ConstTermHandle type)
+// to a reference that is a partition representative.
+// Note "ref" must be a partition representative.
+func (p *Partitions) PartitionFieldMap(ref Reference) FieldMap {
+	return p.partitions[ref].fieldRefs
+}
+
+// Pretty printer
+func (p *Partitions) String() string {
+	state := state{partitions: p.partitions, parents: p.parents}
+	return state.String()
+}
+
+// FieldParents returns the parents of a field reference. Return nil if ref has no parent.
+// For example, return o for r if "o[x -> r]" is in state. This shall be
+// called only after the state has been finalized.
+func (p *Partitions) FieldParents(ref Reference) []Reference {
+	return p.revFields[ref]
+}
+
+// constructFieldParentMap constructs (from scratch) a map from each field reference
+// to its parent reference. For example, record r->o for "o.x = r".
+func (p *Partitions) constructFieldParentMap() {
+	reps := p.Representatives()
+	for rep := range reps {
+		if fmap := p.PartitionFieldMap(rep); fmap != nil {
+			for _, fref := range fmap {
+				p.revFields[fref] = append(p.revFields[fref], rep)
+			}
+		}
+	}
+}
+
+// NumPartitions returns the number of representatives
+// (i.e., the size of the partition).
+func (p *Partitions) NumPartitions() int {
+	return len(p.members)
+}
+
+// MembersForRep gets the list of members in the partition
+// for which "prep" is the representative.
+// Precondition: "prep" must be a partition representative.
+func (p *Partitions) MembersForRep(rep Reference) []Reference {
+	members, ok := p.members[rep]
+	if !ok {
+		log.Printf("Warning: reference [%s] is not a partition rep", rep)
+		return nil
+	}
+	return members
+}
+
+// PartitionMembers gets the list of members in the partition to which "ref" belongs.
+// Precondition: "ref" must be a reference present in the state.
+func (p *Partitions) PartitionMembers(ref Reference) []Reference {
+	return p.MembersForRep(p.Representative(ref))
+}
diff --git a/internal/pkg/levee/levee.go b/internal/pkg/levee/levee.go
index 6ac3fa0..26adb7e 100644
--- a/internal/pkg/levee/levee.go
+++ b/internal/pkg/levee/levee.go
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package internal
+package levee
 
 import (
 	"fmt"
diff --git a/internal/pkg/propagation/propagation.go b/internal/pkg/propagation/propagation.go
index 7c23463..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"
@@ -102,10 +101,6 @@ func (prop *Propagation) shouldNotTaint(n ssa.Node, maxInstrReached map[*ssa.Bas
 		return true
 	}
 
-	if !hasTaintableType(n) {
-		return true
-	}
-
 	if instr, ok := n.(ssa.Instruction); ok {
 		instrIndex, ok := indexInBlock(instr)
 		if !ok {
@@ -225,6 +220,9 @@ func (prop *Propagation) taintField(n ssa.Node, maxInstrReached map[*ssa.BasicBl
 }
 
 func (prop *Propagation) taintReferrers(n ssa.Node, maxInstrReached map[*ssa.BasicBlock]int, lastBlockVisited *ssa.BasicBlock) {
+	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) {
@@ -418,7 +386,7 @@ func hasTaintableType(n ssa.Node) bool {
 	if v, ok := n.(ssa.Value); ok {
 		switch t := v.Type().(type) {
 		case *types.Basic:
-			return t.Info() != types.IsBoolean
+			return (t.Info() & types.IsString) != 0
 		case *types.Signature:
 			return false
 		}
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..d52d035
--- /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..8b2597d
--- /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/source/source.go b/internal/pkg/source/source.go
index 9a70ce2..321ca9c 100644
--- a/internal/pkg/source/source.go
+++ b/internal/pkg/source/source.go
@@ -147,12 +147,13 @@ func isSourceNode(n ssa.Node, conf *config.Config, propagators fieldpropagator.R
 		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
-	// identify the Source from the Extract.
+	// In some cases, an extracted value isn't tied to any other instruction that could be used
+	// to detect a source value, so the Extract itself has to be used. Specifically:
+	// - If the extracted value is a Pointer to a Source
+	// - If the extracted value is inlined into a call
 	case *ssa.Extract:
 		t := v.Tuple.Type().(*types.Tuple).At(v.Index).Type()
-		_, ok := t.(*types.Pointer)
-		return ok && sourcetype.IsSourceType(conf, taggedFields, t)
+		return sourcetype.IsSourceType(conf, taggedFields, t)
 
 	// Unary operator <- can receive sources from a channel.
 	case *ssa.UnOp:
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
 }
diff --git a/pkg/levee/levee.go b/pkg/levee/levee.go
index 84690f3..9504e32 100644
--- a/pkg/levee/levee.go
+++ b/pkg/levee/levee.go
@@ -15,7 +15,26 @@
 // Package levee exports the levee Analyzer.
 package levee
 
-import internal "github.com/google/go-flow-levee/internal/pkg/levee"
+import (
+	"github.com/google/go-flow-levee/internal/pkg/config"
+	"github.com/google/go-flow-levee/internal/pkg/levee"
+	"github.com/google/go-flow-levee/internal/pkg/propagation/summary"
+)
 
 // Analyzer reports instances of source data reaching a sink.
-var Analyzer = internal.Analyzer
+var Analyzer = levee.Analyzer
+
+// SetBytes is a wrapper around the config package's SetBytes function.
+var SetBytes = config.SetBytes
+
+// Summary is a wrapper around the propagation/summary
+// package's Summary type.
+type Summary = summary.Summary
+
+// FuncSummaries is a wrapper around the propagation/summary
+// package's map of regular function summaries.
+var FuncSummaries = summary.FuncSummaries
+
+// InterfaceFuncSummaries is a wrapper around the propagation/summary
+// package's map of interface function summaries.
+var InterfaceFuncSummaries = summary.InterfaceFuncSummaries


Test patch:
diff --git a/internal/pkg/levee/testdata/src/levee_analysistest/example/tests/method/tests.go b/internal/pkg/config/set_config_bytes_test.go
similarity index 58%
rename from internal/pkg/levee/testdata/src/levee_analysistest/example/tests/method/tests.go
rename to internal/pkg/config/set_config_bytes_test.go
index 85ef99b..22e36ee 100644
--- a/internal/pkg/levee/testdata/src/levee_analysistest/example/tests/method/tests.go
+++ b/internal/pkg/config/set_config_bytes_test.go
@@ -12,18 +12,26 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package method
+package config
 
 import (
-	"levee_analysistest/example/core"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
 )
 
-func TestMethodCallOnStaticallyKnownReceiverPropagatesTaint(s core.Source) {
-	data := s.Propagate(s.Data)
-	core.Sink(data) // want "a source has reached a sink"
-}
+func TestSetConfigBytes(t *testing.T) {
+	set := &Config{ReportMessage: "test"}
+	bytes := []byte(`ReportMessage: "test"`)
+
+	SetBytes(bytes)
+
+	read, err := ReadConfig()
+	if err != nil {
+		t.Fatalf("ReadConfig returned an unexpected error: %v", err)
+	}
 
-func TestMethodCallOnStaticallyUnknownReceiverPropagatesTaint(sm core.SourceManipulator, s core.Source) {
-	data := sm.Propagate(s.Data)
-	core.Sink(data) // want "a source has reached a sink"
+	if diff := cmp.Diff(set, read); diff != "" {
+		t.Errorf("set config differs from read config (-set, +read):\n%s", diff)
+	}
 }
diff --git a/internal/pkg/earpointer/state_test.go b/internal/pkg/earpointer/state_test.go
new file mode 100644
index 0000000..31d115d
--- /dev/null
+++ b/internal/pkg/earpointer/state_test.go
@@ -0,0 +1,255 @@
+// Copyright 2021 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 earpointer_test
+
+import (
+	"go/ast"
+	"go/importer"
+	"go/parser"
+	"go/token"
+	"go/types"
+	"sort"
+	"testing"
+
+	"github.com/google/go-flow-levee/internal/pkg/earpointer"
+	"golang.org/x/tools/go/ssa"
+	"golang.org/x/tools/go/ssa/ssautil"
+)
+
+// buildSSA parses the source code, convert it to SSA form, and return the SSA package.
+// The filename for source is always "test.go", and the package is "t" at an empty path.
+func buildSSA(src string) (*ssa.Package, error) {
+	// Parse the source files.
+	fset := token.NewFileSet()
+	f, err := parser.ParseFile(fset, "test.go", src, parser.ParseComments)
+	if err != nil {
+		return nil, err
+	}
+	files := []*ast.File{f}
+
+	// Create the type-checker's package.
+	pkg := types.NewPackage("t", "")
+	// Type-check the package, load dependencies.
+	// Create and build the SSA program.
+	ssaPkg, _, err := ssautil.BuildPackage(
+		&types.Config{Importer: importer.Default()}, fset, pkg, files, ssa.SanityCheckFunctions)
+	if err != nil {
+		return nil, err
+	}
+	return ssaPkg, nil
+}
+
+// Test basic state operations using single global variables.
+// No fields are involved.
+func TestBasic(t *testing.T) {
+	code := `package p
+	type T struct {}
+	var g1 *int
+	var g2 *int
+	var g3 T
+	`
+	pkg, err := buildSSA(code)
+	if err != nil {
+		t.Fatalf("compilation failed: %s", code)
+	}
+	state := earpointer.NewState()
+	getGlobal := func(name string) earpointer.Reference {
+		return earpointer.MakeGlobal(pkg.Members[name].(*ssa.Global))
+	}
+	refs := map[string]earpointer.Reference{
+		"g1": getGlobal("g1"),
+		"g2": getGlobal("g2"),
+		"g3": getGlobal("g3"),
+	}
+	for _, r := range refs {
+		state.Insert(r)
+	}
+
+	want := "{t.g1}: [], {t.g2}: [], {t.g3}: []"
+	if got := state.String(); got != want {
+		t.Errorf("initial:\n got: %s\n want: %s", got, want)
+	}
+
+	state.Unify(refs["g1"], refs["g2"])
+	want = "{t.g1,t.g2}: [], {t.g3}: []"
+	if got := state.String(); got != want {
+		t.Errorf("after unifying g1 and g2:\n got: %s\n want: %s", got, want)
+	}
+
+	// Test the Partitions.
+	// The members information is built after the state is finalized.
+	partitions := state.ToPartitions()
+	if !partitions.Has(refs["g1"]) {
+		t.Errorf("g1 should be in the partitions")
+	}
+	if len(partitions.References()) != 3 {
+		t.Errorf("partitions should contain 3 references")
+	}
+	if partitions.NumPartitions() != 2 {
+		t.Errorf("should have 2 partitions")
+	}
+	members := partitions.PartitionMembers(refs["g1"])
+	if len(members) != 2 {
+		t.Errorf("g1's partition should have 2 members")
+	}
+	mstrs := []string{members[0].String(), members[1].String()}
+	sort.Strings(mstrs)
+	if mstrs[0] != "t.g1" || mstrs[1] != "t.g2" {
+		t.Errorf("g1's partition should contain [t.g1, t.g2]")
+	}
+	want = "{t.g1,t.g2}: [], {t.g3}: []"
+	if got := partitions.String(); got != want {
+		t.Errorf("after unifying g1 and g2:\n got: %s\n want: %s", got, want)
+	}
+}
+
+// Test field unification on global variables.
+func TestGlobalField(t *testing.T) {
+	code := `package p
+	type T struct { x *int; y *int }
+	var g1 *T
+	var g2 *T
+	var g3 *T
+	var g4 *T
+	`
+	pkg, err := buildSSA(code)
+	if err != nil {
+		t.Fatalf("compilation failed: %s", code)
+	}
+	state := earpointer.NewState()
+	getGlobal := func(name string) earpointer.Reference {
+		return earpointer.MakeGlobal(pkg.Members[name].(*ssa.Global))
+	}
+	refs := map[string]earpointer.Reference{
+		"g1": getGlobal("g1"),
+		"g2": getGlobal("g2"),
+		"g3": getGlobal("g3"),
+		"g4": getGlobal("g4"),
+	}
+	for _, r := range refs {
+		state.Insert(r)
+	}
+
+	fm1 := state.PartitionFieldMap(refs["g1"])
+	fm1[earpointer.Field{Name: "x"}] = refs["g3"]
+	want := "{t.g1}: [x->t.g3], {t.g2}: [], {t.g3}: [], {t.g4}: []"
+	if got := state.String(); got != want {
+		t.Errorf("initial:\n got: %s\n want: %s", got, want)
+	}
+
+	// Unify "g3" and "g4", which will also unify "g1" and "g2"
+	fm3 := state.PartitionFieldMap(refs["g3"])
+	fm3[earpointer.Field{Name: "y"}] = refs["g1"]
+	fm4 := state.PartitionFieldMap(refs["g4"])
+	fm4[earpointer.Field{Name: "y"}] = refs["g2"]
+	state.Unify(refs["g3"], refs["g4"])
+	want = "{t.g1,t.g2}: [x->t.g3], {t.g3,t.g4}: [y->t.g2]"
+	if got := state.String(); got != want {
+		t.Errorf("after unifying g3 and g4:\n got: %s\n want: %s", got, want)
+	}
+
+	// Test the Partitions.
+	partitions := state.ToPartitions()
+	want = "{t.g1,t.g2}: [x->t.g4], {t.g3,t.g4}: [y->t.g2]"
+	if got := partitions.String(); got != want {
+		t.Errorf("partitions:\n got: %s\n want: %s", got, want)
+	}
+}
+
+// Test field unification on local variables including parameters and registers.
+func TestLocalField(t *testing.T) {
+	code := `package p
+	type T struct { x *int; y *int }
+	func f(a *T, b *T) {
+		var c T
+		_ = c
+	}
+	`
+	pkg, err := buildSSA(code)
+	if err != nil {
+		t.Fatalf("compilation failed: %s", code)
+	}
+	state := earpointer.NewState()
+	f := pkg.Members["f"].(*ssa.Function)
+	var emptyContext earpointer.Context
+	getLocal := func(param ssa.Value) earpointer.Reference {
+		r := earpointer.MakeLocal(&emptyContext, param)
+		state.Insert(r)
+		return r
+	}
+	refa := getLocal(f.Params[0])
+	refb := getLocal(f.Params[1])
+	refc := getLocal(f.Locals[0])
+
+	want := "{f.a}: [], {f.b}: [], {f.t0}: []"
+	if got := state.String(); got != want {
+		t.Errorf("initial:\n got: %s\n want: %s", got, want)
+	}
+
+	fma := state.PartitionFieldMap(refa)
+	fma[earpointer.Field{Name: "x"}] = refc
+	fma[earpointer.Field{Name: "y"}] = refb
+	fmc := state.PartitionFieldMap(refc)
+	fmc[earpointer.Field{Name: "y"}] = refa
+	want = "{f.a}: [x->f.t0, y->f.b], {f.b}: [], {f.t0}: [y->f.a]"
+	if got := state.String(); got != want {
+		t.Errorf("after setting fields:\n got: %s\n want: %s", got, want)
+	}
+
+	// unify parameter "a" and local "c".
+	state.Unify(refa, refc)
+	want = "{f.a,f.b,f.t0}: [x->f.t0, y->f.a]"
+	if got := state.String(); got != want {
+		t.Errorf("after unifying 'a' and 'b':\n got: %s\n want: %s", got, want)
+	}
+
+	// Test the Partitions.
+	partitions := state.ToPartitions()
+	want = "{f.a,f.b,f.t0}: [x->f.t0, y->f.t0]"
+	if got := partitions.String(); got != want {
+		t.Errorf("partitions:\n got: %s\n want: %s", got, want)
+	}
+}
+
+func TestSyntheticReference(t *testing.T) {
+	code := `package p
+	var g1 *int
+	`
+	pkg, err := buildSSA(code)
+	if err != nil {
+		t.Fatalf("compilation failed: %s", code)
+	}
+	g1 := pkg.Members["g1"].(*ssa.Global)
+	ref1 := earpointer.MakeGlobal(g1)
+
+	i1 := earpointer.MakeSynthetic(earpointer.SyntheticValueOf, ref1)
+	i2 := earpointer.MakeSynthetic(earpointer.SyntheticValueOf, ref1)
+	if i1 != i2 {
+		t.Errorf("[%s] != [%s]", i1, i2)
+	}
+	want := "*t.g1"
+	if got := i1.String(); got != want {
+		t.Errorf("String():\n got: %s\n want: %s", got, want)
+	}
+
+	i3 := earpointer.MakeSynthetic(earpointer.SyntheticField, ref1)
+	if i1 == i3 {
+		t.Errorf("[%s] == [%s]", i1, i3)
+	}
+	want = "t.g1[.]"
+	if got := i3.String(); got != want {
+		t.Errorf("String():\n got: %s\n want: %s", got, want)
+	}
+}
diff --git a/internal/pkg/levee/levee_test.go b/internal/pkg/levee/levee_test.go
index 3e680a1..22363cf 100644
--- a/internal/pkg/levee/levee_test.go
+++ b/internal/pkg/levee/levee_test.go
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package internal
+package levee
 
 import (
 	"flag"
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/makeinterface/tests.go b/internal/pkg/levee/testdata/src/levee_analysistest/example/tests/basictypes/tests.go
similarity index 67%
rename from internal/pkg/levee/testdata/src/levee_analysistest/example/tests/makeinterface/tests.go
rename to internal/pkg/levee/testdata/src/levee_analysistest/example/tests/basictypes/tests.go
index 0c543fc..95b4e9a 100644
--- a/internal/pkg/levee/testdata/src/levee_analysistest/example/tests/makeinterface/tests.go
+++ b/internal/pkg/levee/testdata/src/levee_analysistest/example/tests/basictypes/tests.go
@@ -12,17 +12,22 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package deletethis
+package basic
 
 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 BooleansDontPropagateTaint(s *core.Source) {
+	is := isSourcey(s)
+	core.Sink(is)
 }
 
-func colocate(s *core.Source, x interface{}) {}
+func isSourcey(x interface{}) bool {
+	// not taking any chances
+	return true
+}
+
+func IntegersDontPropagateTaint(sources []core.Source) {
+	core.Sink(len(sources))
+}
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 12a930f..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)   // want "a source has reached a sink"
-	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/inlining/tests.go b/internal/pkg/levee/testdata/src/levee_analysistest/example/tests/inlining/tests.go
index 3135c05..78d165a 100644
--- a/internal/pkg/levee/testdata/src/levee_analysistest/example/tests/inlining/tests.go
+++ b/internal/pkg/levee/testdata/src/levee_analysistest/example/tests/inlining/tests.go
@@ -22,10 +22,23 @@ func NewSource() *core.Source {
 	return &core.Source{}
 }
 
+func MaybeSource() (core.Source, error) {
+	return core.Source{}, nil
+}
+
+func MaybeSourcePtr() (*core.Source, error) {
+	return &core.Source{}, nil
+}
+
 func TestInlinedCall() {
 	core.Sink(NewSource()) // want "a source has reached a sink"
 }
 
+func TestInlinedTupleCall() {
+	core.Sink(MaybeSource())    // want "a source has reached a sink"
+	core.Sink(MaybeSourcePtr()) // want "a source has reached a sink"
+}
+
 func TestInlinedRecv(sources <-chan core.Source) {
 	core.Sink(<-sources) // want "a source has reached a sink"
 }
diff --git a/internal/pkg/levee/testdata/src/levee_analysistest/example/tests/propagation/builtins.go b/internal/pkg/levee/testdata/src/levee_analysistest/example/tests/propagation/builtins.go
index 53d26f6..7fcfa1a 100644
--- a/internal/pkg/levee/testdata/src/levee_analysistest/example/tests/propagation/builtins.go
+++ b/internal/pkg/levee/testdata/src/levee_analysistest/example/tests/propagation/builtins.go
@@ -20,7 +20,8 @@ import (
 
 func TestCopyPropagatesTaintFromSrcToDst(s core.Source) {
 	b := make([]byte, len(s.Data))
-	copy(b, s.Data)
+	bytesCopied := copy(b, s.Data)
+	core.Sink(bytesCopied)
 	core.Sink(b) // 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 05aa40c..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,31 +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, 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"
+	}
 }
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{}
diff --git a/internal/pkg/source/testdata/src/analyzertest/sourcetest/identification.go b/internal/pkg/source/testdata/src/analyzertest/sourcetest/identification.go
index 61dc95e..827cde4 100644
--- a/internal/pkg/source/testdata/src/analyzertest/sourcetest/identification.go
+++ b/internal/pkg/source/testdata/src/analyzertest/sourcetest/identification.go
@@ -88,15 +88,24 @@ func TestNamedReturnValues() (val Source, ptr *Source) { // want "source identif
 }
 
 func TestSourceExtracts() {
-	s, err := CreateSource() // want "source identified"
+	// We expect two reports for this case, because creating s
+	// will require an Extract and an Alloc.
+	s, err := CreateSource() // want "source identified" "source identified"
 	sptr, err := NewSource() // want "source identified"
 
-	// we expect two reports for the following cases, since the map is a Source
-	mapSource, ok := map[string]Source{}[""]     // want "source identified" "source identified"
+	// We expect three reports for this case, because:
+	// 1. the map is a Source
+	// 2. there is an Extract for the mapSource value
+	// 3. there is an Alloc for the mapSource value
+	mapSource, ok := map[string]Source{}[""] // want "source identified" "source identified" "source identified"
+
+	// We expect two reports here, for the map and the Extract.
+	// (There won't be an Alloc because mapSourcePtr is a pointer.)
 	mapSourcePtr, ok := map[string]*Source{}[""] // want "source identified" "source identified"
 
-	// we expect two reports for the following cases, since the chan is a Source
-	chanSource, ok := <-(make(chan Source))     // want "source identified" "source identified"
+	// These two cases are similar to the map cases above.
+	// The reasoning behind the number of expected reports is the same.
+	chanSource, ok := <-(make(chan Source))     // want "source identified" "source identified" "source identified"
 	chanSourcePtr, ok := <-(make(chan *Source)) // want "source identified" "source identified"
 
 	_, _, _, _, _, _, _, _ = s, sptr, mapSource, chanSource, mapSourcePtr, chanSourcePtr, err, ok