Repository: google/go-flow-levee
Pull request #43 (2020-08-06 20:23:15)

Introduce debugging enchantments
Introduce code I've been using to produce SSA source + DOT source for quick perusal/visualization of test functions' SSA code.

This is a quick PR to see if you think that this approach is acceptable. I think it doesn't add too much clutter and will be very useful.

Caveats:
* Currently, the code is untested. This is because I have been iterating on it rapidly, and I believe tests would have merely slowed me down.

Misc. : 
* Should I mention this in the README?
* You may want to put a small script somewhere on your path to quickly generate PDF's or PNG's from DOT files 
e.g. `dotpdf.sh` : 
```sh
for f in "$@"; do
	dot -Tpdf "$f" -o "$(basename "$f" .dot).pdf"
done
```
E.g. : `dotpdf sinks_TestSinks.dot`

- [x] Tests pass
- [x] Appropriate changes to README are included in PR

mlevesquedion: > I strongly discourage this approach. While I don't fully advocate for test driven development, it's difficult for me to meaningfully review code that is failing tests. `levee_test` is currently failing regardless of whether or not `debugging` is set to `true`.

I'm not sure what you're talking about. If I set debugging to `false` the tests in `levee_test.go` run just fine. 
mlevesquedion: Question: where should I put the documentation for a package if it has no obvious "primary" file? Specifically, I'm thinking about `debug/render`. This package contains `dot.go` and `ssa.go`, neither of which is more important. I don't think it would be a good idea to duplicate the documentation. I could create a `doc.go` file specifically for the documentation. Right now this seems to me to be the best approach, do you see something else that would be preferable? 
mlevesquedion: Question: should I document this in the README?
immutableT: I think this looks very good now.

I know there are some open questions on how to incorporate the `debug.Analyzer` into an arbitrary test, but I am OK to do this manually for now.
The only think I would ask for is a README file that would explain the process of integrating the debug.Analyzer into a test and consuming its output.

mlevesquedion: Great! :tada: Done.

Here are my plans for future PRs:
1. Add tests
2. Add support for methods (currently, only `static` functions are handled).

Patch:
diff --git a/.gitignore b/.gitignore
index 84cbd40..6d4639c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,5 @@
 # Default ignored files
 /.idea
+
+# Debugging output
+output
diff --git a/README.md b/README.md
index 94c7526..823dec7 100644
--- a/README.md
+++ b/README.md
@@ -32,6 +32,28 @@ See [design/](design/README.md).
 
 See [configuration/](configuration/README.md) for configuration details.
 
+### Debugging
+
+The main analyzer depends heavily on the SSA package. Being able to read the SSA code and visualize its graph can be very useful for debugging. In order to generate the SSA code and DOT (graphviz) source for every function in a test, run `go test levee_test.go -debug`. Results are written to the `output` directory. You can generate a PDF from the DOT source using `dot -Tpdf <file> -o "$(basename <file> .dot).pdf"`.
+
+Currently, debugging is only supported for `levee_test.go`. In order to add support for debugging in a new test, first add a debugging flag:
+```go
+var debugging bool = flag.Bool("debug", false, "run the debug analyzer")
+```
+Then add `debug.Analyzer` as a dependency of the analyzer being tested:
+```go
+if *debugging {
+	Analyzer.Requires = append(Analyzer.Requires, debug.Analyzer)
+}
+```
+
+In the `dot` output:
+* A **red** edge points to a **Referrer** of an `ssa.Node`
+* An **orange** edges points to an **Operand** of an `ssa.Node`
+* **Diamond**-shaped nodes represent `ssa.Node`s that are both `ssa.Instruction`s and `ssa.Value`s
+* **Square**-shaped node represent `ssa.Node`s that are only `ssa.Instruction`s
+* **Ellipse**-shaped nodes that are either only `ssa.Value`s, or are `ssa.Member`s.
+
 ## Source Code Headers
 
 Every file containing source code must include copyright and license
diff --git a/internal/pkg/config/config.go b/internal/pkg/config/config.go
index 117ecb2..db4e78a 100644
--- a/internal/pkg/config/config.go
+++ b/internal/pkg/config/config.go
@@ -84,6 +84,16 @@ func (c Config) IsSink(call *ssa.Call) bool {
 	return false
 }
 
+func (c Config) IsSinkFunction(f *ssa.Function) bool {
+	for _, p := range c.Sinks {
+		if p.MethodRE.MatchString(f.Name()) {
+			return true
+		}
+	}
+
+	return false
+}
+
 func (c Config) IsSanitizer(call *ssa.Call) bool {
 	for _, p := range c.Sanitizers {
 		if p.Match(call) {
diff --git a/internal/pkg/debug/debug.go b/internal/pkg/debug/debug.go
new file mode 100644
index 0000000..da682b4
--- /dev/null
+++ b/internal/pkg/debug/debug.go
@@ -0,0 +1,40 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package debug
+
+import (
+	"github.com/google/go-flow-levee/internal/pkg/debug/dump"
+	"golang.org/x/tools/go/analysis"
+	"golang.org/x/tools/go/analysis/passes/buildssa"
+)
+
+var Analyzer = &analysis.Analyzer{
+	Name:     "debug",
+	Run:      run,
+	Doc:      "dumps SSA and DOT source for every function",
+	Requires: []*analysis.Analyzer{buildssa.Analyzer},
+}
+
+func run(pass *analysis.Pass) (interface{}, error) {
+	ssaInput := pass.ResultOf[buildssa.Analyzer].(*buildssa.SSA)
+
+	for _, f := range ssaInput.SrcFuncs {
+		pkgName := f.Pkg.Pkg.Name()
+		dump.SSA(pkgName, f)
+		dump.DOT(pkgName, f)
+	}
+
+	return nil, nil
+}
diff --git a/internal/pkg/debug/dump/dump.go b/internal/pkg/debug/dump/dump.go
new file mode 100644
index 0000000..a1a8f72
--- /dev/null
+++ b/internal/pkg/debug/dump/dump.go
@@ -0,0 +1,54 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package dump contains functions for writing a function's SSA as SSA or DOT source to a file.
+package dump
+
+import (
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"runtime"
+	"strings"
+
+	"github.com/google/go-flow-levee/internal/pkg/debug/render"
+	"golang.org/x/tools/go/ssa"
+)
+
+// SSA dumps a function's SSA to a file.
+func SSA(fileName string, f *ssa.Function) {
+	save(fileName, f.Name(), render.SSA(f), "ssa")
+}
+
+// DOT dumps DOT source representing the function's SSA graph to a file.
+func DOT(fileName string, f *ssa.Function) {
+	save(fileName, f.Name(), render.DOT(f), "dot")
+}
+
+func save(fileName, funcName, s, ending string) {
+	baseName := strings.TrimSuffix(fileName, ".go")
+	outFile := fmt.Sprintf("%s_%s.%s", baseName, funcName, ending)
+	err := ioutil.WriteFile(filepath.Join(outDir(), outFile), []byte(s), 0666)
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "could not write to file: %s, error: %v\n", outFile, err)
+	}
+}
+
+func outDir() string {
+	_, currentFile, _, _ := runtime.Caller(0)
+	d := filepath.Join(filepath.Dir(currentFile), "../../../../output")
+	os.MkdirAll(d, 0755)
+	return d
+}
diff --git a/internal/pkg/debug/graph/graph.go b/internal/pkg/debug/graph/graph.go
new file mode 100644
index 0000000..5b86ecd
--- /dev/null
+++ b/internal/pkg/debug/graph/graph.go
@@ -0,0 +1,146 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package graph defines an abstraction of the SSA graph that facilitates rendering.
+package graph
+
+import (
+	"log"
+
+	"golang.org/x/tools/go/ssa"
+)
+
+type relationship int
+
+const (
+	// Referrer represents a node that is referred to as per ssa.Value.Referrers
+	Referrer relationship = iota
+	// Operand represents a node that is an operand of an instruction as per ssa.Instr.Operands
+	Operand
+)
+
+// Node represents a node in the SSA graph, along with its relationship to a parent node.
+type Node struct {
+	// N is the ssa.Node wrapped by this type.
+	N ssa.Node
+	// R is the relationship between this node and its parent in the SSA graph.
+	R relationship
+}
+
+// FuncGraph represents the SSA graph for an ssa.Function.
+type FuncGraph struct {
+	// F is the function whose graph this is
+	F *ssa.Function
+	// Children is a mapping from each node to its children (referrers + operands)
+	Children map[ssa.Node][]Node
+	// visited is used while creating the graph to avoid needlessly revisiting nodes
+	visited map[ssa.Node]bool
+	// stack is used to perform a DFS on F's SSA graph
+	s stack
+}
+
+// New returns a new Graph constructed from a given function.
+func New(f *ssa.Function) *FuncGraph {
+	g := FuncGraph{
+		F:        f,
+		Children: map[ssa.Node][]Node{},
+		visited:  map[ssa.Node]bool{},
+	}
+	g.visitBlocks()
+	return &g
+}
+
+func (g *FuncGraph) visitBlocks() {
+	for _, b := range g.F.Blocks {
+		g.visit(b)
+	}
+}
+
+func (g *FuncGraph) visit(b *ssa.BasicBlock) {
+	// according to the ssa package docs, this should not happen,
+	// but we don't want a panic
+	if len(b.Instrs) == 0 {
+		return
+	}
+
+	n := b.Instrs[0].(ssa.Node)
+	g.visited[n] = true
+	g.s.push(n)
+	for len(g.s) > 0 {
+		current := g.s.pop()
+		if current == nil {
+			break
+		}
+		g.visitOperands(current)
+		g.visitReferrers(current)
+	}
+}
+
+func (g *FuncGraph) visitOperands(n ssa.Node) {
+	var operands []*ssa.Value
+	operands = n.Operands(operands)
+	if operands == nil {
+		return
+	}
+	for _, o := range operands {
+		on, ok := (*o).(ssa.Node)
+		if !ok {
+			continue
+		}
+		g.addOperand(n, on)
+		g.visitNode(on)
+	}
+}
+
+func (g *FuncGraph) visitReferrers(n ssa.Node) {
+	if n.Referrers() == nil {
+		return
+	}
+	for _, ref := range *n.Referrers() {
+		rn := ref.(ssa.Node)
+		g.addReferrer(n, rn)
+		g.visitNode(rn)
+	}
+}
+
+func (g *FuncGraph) visitNode(n ssa.Node) {
+	if g.visited[n] {
+		return
+	}
+	g.visited[n] = true
+	g.s.push(n)
+}
+
+func (g *FuncGraph) addReferrer(current, referrer ssa.Node) {
+	g.Children[current] = append(g.Children[current], Node{N: referrer, R: Referrer})
+}
+
+func (g *FuncGraph) addOperand(current, operand ssa.Node) {
+	g.Children[current] = append(g.Children[current], Node{N: operand, R: Operand})
+}
+
+type stack []ssa.Node
+
+func (s *stack) pop() ssa.Node {
+	if len(*s) == 0 {
+		log.Println("tried to pop from empty stack")
+	}
+	popped := (*s)[len(*s)-1]
+	*s = (*s)[:len(*s)-1]
+	return popped
+}
+
+func (s *stack) push(n ssa.Node) {
+	*s = append(*s, n)
+}
diff --git a/internal/pkg/debug/node/node.go b/internal/pkg/debug/node/node.go
new file mode 100644
index 0000000..448ef8b
--- /dev/null
+++ b/internal/pkg/debug/node/node.go
@@ -0,0 +1,49 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package node contains utility functions for working with SSA nodes.
+package node
+
+import (
+	"fmt"
+	"strings"
+
+	"golang.org/x/tools/go/ssa"
+)
+
+// CanonicalName produces a canonical string representation for an SSA node.
+func CanonicalName(n ssa.Node) string {
+	value, isValue := n.(ssa.Value)
+	instr, isInstr := n.(ssa.Instruction)
+	switch {
+	case isValue && isInstr:
+		return fmt.Sprintf("%s = %s", value.Name(), instr.String())
+	case isValue:
+		return value.Name()
+	case isInstr:
+		return instr.String()
+	default:
+		member, isMember := n.(ssa.Member)
+		if !isMember {
+			return ""
+		}
+		return member.Name()
+	}
+}
+
+// TrimmedType returns the type of a node without the "*.ssa" prefix.
+func TrimmedType(n ssa.Node) string {
+	t := fmt.Sprintf("%T", n)
+	return strings.TrimPrefix(t, "*ssa.")
+}
diff --git a/internal/pkg/debug/render/dot.go b/internal/pkg/debug/render/dot.go
new file mode 100644
index 0000000..33b84f6
--- /dev/null
+++ b/internal/pkg/debug/render/dot.go
@@ -0,0 +1,111 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package render
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/google/go-flow-levee/internal/pkg/debug/graph"
+	"github.com/google/go-flow-levee/internal/pkg/debug/node"
+	"golang.org/x/tools/go/ssa"
+)
+
+// DOT produces DOT source code representing the SSA graph for a function.
+func DOT(f *ssa.Function) string {
+	return renderDOT(graph.New(f))
+}
+
+func renderDOT(g *graph.FuncGraph) string {
+	return (&renderer{strings.Builder{}, g}).Render()
+}
+
+type renderer struct {
+	strings.Builder
+	*graph.FuncGraph
+}
+
+func (r *renderer) Render() string {
+	r.init()
+	r.writeSubgraphs()
+	r.writeEdges()
+	r.finish()
+	return r.String()
+}
+
+func (r *renderer) init() {
+	_, _ = r.WriteString("digraph {\n")
+}
+
+func (r *renderer) writeSubgraphs() {
+	for bi, b := range r.F.Blocks {
+		_, _ = r.WriteString(fmt.Sprintf("\tsubgraph cluster_%d {\n\t\tcolor=black;\n\t\tlabel=%q;\n", bi, b.Comment))
+		for _, i := range b.Instrs {
+			n := i.(ssa.Node)
+			_, _ = r.WriteString(fmt.Sprintf("\t\t%q [shape=%s];\n", renderNode(n), nodeShape(n)))
+		}
+		_, _ = r.WriteString("\t}\n")
+	}
+}
+
+func (r *renderer) writeEdges() {
+	for from, children := range r.Children {
+		for _, to := range children {
+			switch to.R {
+			case graph.Referrer:
+				r.addReferrer(from, to.N)
+			case graph.Operand:
+				r.addOperand(from, to.N)
+			}
+		}
+	}
+}
+
+func (r *renderer) addReferrer(n ssa.Node, ref ssa.Node) {
+	// TODO: document this somewhere?
+	// Red as in R-eferrer
+	r.addEdge(n, ref, "red")
+}
+
+func (r *renderer) addOperand(n ssa.Node, op ssa.Node) {
+	// TODO: document this somewhere?
+	// Orange as in O-perand
+	r.addEdge(n, op, "orange")
+}
+
+func (r *renderer) addEdge(from ssa.Node, to ssa.Node, color string) {
+	_, _ = r.WriteString(fmt.Sprintf("\t%q -> %q [color=%s];\n", renderNode(from), renderNode(to), color))
+}
+
+func renderNode(n ssa.Node) string {
+	return fmt.Sprintf("%s\n(%s)", node.CanonicalName(n), node.TrimmedType(n))
+}
+
+func (r *renderer) finish() {
+	_, _ = r.WriteString("}\n")
+}
+
+func nodeShape(n ssa.Node) string {
+	_, isValue := n.(ssa.Value)
+	_, isInstr := n.(ssa.Instruction)
+	switch {
+	case isValue && isInstr:
+		return "diamond"
+	case isInstr:
+		return "square"
+	default:
+		return "ellipse"
+	}
+}
diff --git a/internal/pkg/debug/render/ssa.go b/internal/pkg/debug/render/ssa.go
new file mode 100644
index 0000000..f9ace7d
--- /dev/null
+++ b/internal/pkg/debug/render/ssa.go
@@ -0,0 +1,41 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package render
+
+import (
+	"fmt"
+	"strconv"
+	"strings"
+
+	"github.com/google/go-flow-levee/internal/pkg/debug/node"
+	"golang.org/x/tools/go/ssa"
+)
+
+// SSA produces a human-readable representation of the SSA code for a function.
+func SSA(f *ssa.Function) string {
+	var b strings.Builder
+	for i, blk := range f.Blocks {
+		b.WriteString(fmt.Sprintf("%d: %s\n", i, blk.Comment))
+		for j, instr := range blk.Instrs {
+			s := node.CanonicalName(instr.(ssa.Node))
+			_ = b.WriteByte('\t')
+			_, _ = b.WriteString(strconv.Itoa(j))
+			_, _ = b.WriteString(fmt.Sprintf("(%-20T): ", instr))
+			_, _ = b.WriteString(s)
+			_ = b.WriteByte('\n')
+		}
+	}
+	return b.String()
+}
diff --git a/internal/pkg/fieldpropagator/analyzer.go b/internal/pkg/fieldpropagator/analyzer.go
new file mode 100644
index 0000000..c6ae602
--- /dev/null
+++ b/internal/pkg/fieldpropagator/analyzer.go
@@ -0,0 +1,118 @@
+// 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 fieldpropagator implements identification of field propagators.
+// A field propagator is a function that returns a source field.
+package fieldpropagator
+
+import (
+	"go/types"
+	"reflect"
+
+	"github.com/google/go-flow-levee/internal/pkg/config"
+	"golang.org/x/tools/go/analysis"
+	"golang.org/x/tools/go/analysis/passes/buildssa"
+	"golang.org/x/tools/go/ssa"
+)
+
+// ResultType is a map telling whether an object (really a function) is a field propagator.
+type ResultType = map[types.Object]bool
+
+type isFieldPropagator struct{}
+
+func (i isFieldPropagator) AFact() {}
+
+func (i isFieldPropagator) String() string {
+	return "field propagator identified"
+}
+
+var Analyzer = &analysis.Analyzer{
+	Name: "fieldpropagator",
+	Doc: `This analyzer identifies field propagators.
+
+A field propagator is a function that returns a source field.`,
+	Flags:      config.FlagSet,
+	Run:        run,
+	Requires:   []*analysis.Analyzer{buildssa.Analyzer},
+	ResultType: reflect.TypeOf(new(ResultType)).Elem(),
+	FactTypes:  []analysis.Fact{new(isFieldPropagator)},
+}
+
+func run(pass *analysis.Pass) (interface{}, error) {
+	ssaInput := pass.ResultOf[buildssa.Analyzer].(*buildssa.SSA)
+
+	conf, err := config.ReadConfig()
+	if err != nil {
+		return nil, err
+	}
+
+	ssaProg := ssaInput.Pkg.Prog
+	for _, mem := range ssaInput.Pkg.Members {
+		ssaType, ok := mem.(*ssa.Type)
+		if !ok || !conf.IsSource(ssaType.Type()) {
+			continue
+		}
+		methods := ssaProg.MethodSets.MethodSet(ssaType.Type())
+		for i := 0; i < methods.Len(); i++ {
+			meth := ssaProg.MethodValue(methods.At(i))
+			analyzeBlocks(pass, conf, meth)
+		}
+	}
+
+	isFieldPropagator := map[types.Object]bool{}
+	for _, f := range pass.AllObjectFacts() {
+		isFieldPropagator[f.Object] = true
+	}
+	return isFieldPropagator, nil
+}
+
+func analyzeBlocks(pass *analysis.Pass, conf *config.Config, meth *ssa.Function) {
+	// Function does not return anything
+	if res := meth.Signature.Results(); res == nil || (*res).Len() == 0 {
+		return
+	}
+	for _, b := range meth.Blocks {
+		if len(b.Instrs) == 0 {
+			continue
+		}
+		lastInstr := b.Instrs[len(b.Instrs)-1]
+		ret, ok := lastInstr.(*ssa.Return)
+		if !ok {
+			continue
+		}
+		analyzeResults(pass, conf, meth, ret.Results)
+	}
+}
+
+func analyzeResults(pass *analysis.Pass, conf *config.Config, meth *ssa.Function, results []ssa.Value) {
+	for _, r := range results {
+		fa, ok := fieldAddr(r)
+		if !ok {
+			continue
+		}
+		if conf.IsSourceFieldAddr(fa) {
+			pass.ExportObjectFact(meth.Object(), &isFieldPropagator{})
+		}
+	}
+}
+
+func fieldAddr(x ssa.Value) (*ssa.FieldAddr, bool) {
+	switch t := x.(type) {
+	case *ssa.FieldAddr:
+		return t, true
+	case *ssa.UnOp:
+		return fieldAddr(t.X)
+	}
+	return nil, false
+}
diff --git a/internal/pkg/source/source.go b/internal/pkg/source/source.go
index dbf5e83..eae79fa 100644
--- a/internal/pkg/source/source.go
+++ b/internal/pkg/source/source.go
@@ -31,6 +31,7 @@ type classifier interface {
 	IsSanitizer(*ssa.Call) bool
 	IsPropagator(*ssa.Call) bool
 	IsSourceFieldAddr(*ssa.FieldAddr) bool
+	IsSinkFunction(fn *ssa.Function) bool
 }
 
 // Source represents a Source in an SSA call tree.
@@ -68,15 +69,22 @@ func (a *Source) dfs(n ssa.Node) {
 	a.preOrder = append(a.preOrder, n)
 	a.marked[n.(ssa.Node)] = true
 
-	if n.Referrers() == nil {
-		return
+	if n.Referrers() != nil {
+		a.visitReferrers(n.Referrers())
 	}
 
-	for _, r := range *n.Referrers() {
+	var operands []*ssa.Value
+	operands = n.Operands(operands)
+	if operands != nil {
+		a.visitOperands(operands)
+	}
+}
+
+func (a *Source) visitReferrers(referrers *[]ssa.Instruction) {
+	for _, r := range *referrers {
 		if a.marked[r.(ssa.Node)] {
 			continue
 		}
-
 		switch v := r.(type) {
 		case *ssa.Call:
 			// This is to avoid attaching calls where the source is the receiver, ex:
@@ -84,7 +92,6 @@ func (a *Source) dfs(n ssa.Node) {
 			if v.Call.Signature().Recv() != nil {
 				continue
 			}
-
 			if a.config.IsSanitizer(v) {
 				a.sanitizers = append(a.sanitizers, &sanitizer.Sanitizer{Call: v})
 			}
@@ -93,11 +100,23 @@ func (a *Source) dfs(n ssa.Node) {
 				continue
 			}
 		}
-
 		a.dfs(r.(ssa.Node))
 	}
 }
 
+func (a *Source) visitOperands(operands []*ssa.Value) {
+	for _, o := range operands {
+		n, ok := (*o).(ssa.Node)
+		if !ok || a.marked[n] {
+			continue
+		}
+		al, ok := (*o).(*ssa.Alloc)
+		if !ok || al.Comment == "slicelit" || a.config.IsSource(al.Type()) {
+			a.dfs(n)
+		}
+	}
+}
+
 // compress removes the elements from the graph that are not required by the
 // taint-propagation analysis. Concretely, only propagators, sanitizers and
 // sinks should constitute the output. Since, we already know what the source
@@ -148,6 +167,11 @@ func identify(conf classifier, ssaInput *buildssa.SSA) map[*ssa.Function][]*Sour
 	sourceMap := make(map[*ssa.Function][]*Source)
 
 	for _, fn := range ssaInput.SrcFuncs {
+		// no need to analyze the body of sinks
+		if conf.IsSinkFunction(fn) {
+			continue
+		}
+
 		var sources []*Source
 		sources = append(sources, sourcesFromParams(fn, conf)...)
 		sources = append(sources, sourcesFromClosure(fn, conf)...)


Test patch:
diff --git a/internal/levee_test.go b/internal/levee_test.go
index b216ef8..68b30fb 100644
--- a/internal/levee_test.go
+++ b/internal/levee_test.go
@@ -15,20 +15,27 @@
 package internal
 
 import (
+	"flag"
 	"io/ioutil"
 	"path/filepath"
 	"testing"
 
+	"github.com/google/go-flow-levee/internal/pkg/debug"
 	"golang.org/x/tools/go/analysis/analysistest"
 )
 
+var debugging *bool = flag.Bool("debug", false, "run the debug analyzer")
+
 func TestLevee(t *testing.T) {
 	dataDir := analysistest.TestData()
-	testsDir := filepath.Join(dataDir, "src/example.com/tests")
-	patterns := findTestPatterns(t, testsDir)
 	if err := Analyzer.Flags.Set("config", dataDir+"/test-config.json"); err != nil {
 		t.Error(err)
 	}
+	testsDir := filepath.Join(dataDir, "src/example.com/tests")
+	patterns := findTestPatterns(t, testsDir)
+	if *debugging {
+		Analyzer.Requires = append(Analyzer.Requires, debug.Analyzer)
+	}
 	analysistest.Run(t, dataDir, Analyzer, patterns...)
 }
 
diff --git a/internal/pkg/fieldpropagator/analyzer_test.go b/internal/pkg/fieldpropagator/analyzer_test.go
new file mode 100644
index 0000000..6dc234d
--- /dev/null
+++ b/internal/pkg/fieldpropagator/analyzer_test.go
@@ -0,0 +1,31 @@
+// 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 fieldpropagator
+
+import (
+	"path/filepath"
+	"testing"
+
+	"github.com/google/go-flow-levee/internal/pkg/config"
+	"golang.org/x/tools/go/analysis/analysistest"
+)
+
+func TestFieldPropagatorAnalysis(t *testing.T) {
+	testdata := analysistest.TestData()
+	if err := config.FlagSet.Set("config", filepath.Join(testdata, "test-config.json")); err != nil {
+		t.Error(err)
+	}
+	analysistest.Run(t, testdata, Analyzer, "source", "test")
+}
diff --git a/internal/pkg/fieldpropagator/testdata/src/source/test.go b/internal/pkg/fieldpropagator/testdata/src/source/test.go
new file mode 100644
index 0000000..931c55a
--- /dev/null
+++ b/internal/pkg/fieldpropagator/testdata/src/source/test.go
@@ -0,0 +1,66 @@
+// 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 source
+
+type Source struct {
+	data    string
+	dataPtr *string
+	id      int
+}
+
+func (s Source) ID() int {
+	return s.id
+}
+
+func (s Source) Data() string { // want Data:"field propagator identified"
+	return s.data
+}
+
+func (s Source) DataRef() *string { // want DataRef:"field propagator identified"
+	return &s.data
+}
+
+func (s Source) DataPtr() *string { // want DataPtr:"field propagator identified"
+	return s.dataPtr
+}
+
+func (s Source) DataDeref() string { // want DataDeref:"field propagator identified"
+	return *s.dataPtr
+}
+
+var isAdmin bool
+
+func (s Source) MaybeData() string { // want MaybeData:"field propagator identified"
+	if isAdmin {
+		return s.data
+	}
+	return "<redacted>"
+}
+
+func (s Source) TryGetData() (string, error) { // want TryGetData:"field propagator identified"
+	return s.data, nil
+}
+
+func New(data string) Source {
+	return Source{data: data, id: 0}
+}
+
+type NotSource struct {
+	data string
+}
+
+func (n NotSource) Data() string {
+	return n.data
+}
diff --git a/internal/pkg/fieldpropagator/testdata/src/test/test.go b/internal/pkg/fieldpropagator/testdata/src/test/test.go
new file mode 100644
index 0000000..82f8638
--- /dev/null
+++ b/internal/pkg/fieldpropagator/testdata/src/test/test.go
@@ -0,0 +1,23 @@
+// 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 "source"
+
+func Test() {
+	s := source.New("secret")
+	d := s.Data()
+	_ = d
+}
diff --git a/internal/pkg/fieldpropagator/testdata/test-config.json b/internal/pkg/fieldpropagator/testdata/test-config.json
new file mode 100644
index 0000000..e06512a
--- /dev/null
+++ b/internal/pkg/fieldpropagator/testdata/test-config.json
@@ -0,0 +1,9 @@
+{
+  "Sources": [
+    {
+      "PackageRE": "source",
+      "TypeRE": "^Source$",
+      "FieldRE": "^data"
+    }
+  ]
+}
diff --git a/internal/pkg/source/source_test.go b/internal/pkg/source/source_test.go
index 5870dda..57d7acf 100644
--- a/internal/pkg/source/source_test.go
+++ b/internal/pkg/source/source_test.go
@@ -32,6 +32,7 @@ type testConfig struct {
 	propagatorsPattern string
 	fieldsPattern      string
 	sanitizerPattern   string
+	sinkPattern        string
 }
 
 func (c *testConfig) IsSource(t types.Type) bool {
@@ -60,6 +61,11 @@ func (c *testConfig) IsSourceFieldAddr(field *ssa.FieldAddr) bool {
 	return match
 }
 
+func (c *testConfig) IsSinkFunction(f *ssa.Function) bool {
+	match, _ := regexp.MatchString(c.sinkPattern, f.Name())
+	return match
+}
+
 var testAnalyzer = &analysis.Analyzer{
 	Name:     "source",
 	Run:      runTest,
@@ -74,6 +80,7 @@ func runTest(pass *analysis.Pass) (interface{}, error) {
 		propagatorsPattern: "propagator",
 		sanitizerPattern:   "sanitizer",
 		fieldsPattern:      "name",
+		sinkPattern:        "sink",
 	}
 
 	sm := identify(config, in)
diff --git a/internal/pkg/varargs/varargs_test.go b/internal/pkg/varargs/varargs_test.go
index ffea747..042eb31 100644
--- a/internal/pkg/varargs/varargs_test.go
+++ b/internal/pkg/varargs/varargs_test.go
@@ -49,6 +49,10 @@ func (c *testConfig) IsSourceFieldAddr(field *ssa.FieldAddr) bool {
 	return false
 }
 
+func (c *testConfig) IsSinkFunction(f *ssa.Function) bool {
+	return false
+}
+
 type analyzerResult struct {
 	allocations []*ssa.Alloc
 	calls       []*ssa.Call
diff --git a/internal/testdata/src/example.com/tests/collections/tests.go b/internal/testdata/src/example.com/tests/collections/tests.go
new file mode 100644
index 0000000..859996e
--- /dev/null
+++ b/internal/testdata/src/example.com/tests/collections/tests.go
@@ -0,0 +1,28 @@
+// 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 collections
+
+import (
+	"example.com/core"
+)
+
+func TestSlices(s core.Source) {
+	slice := []core.Source{s}
+	core.Sink(slice)                      // want "a source has reached a sink"
+	core.Sink([]core.Source{s})           // want "a source has reached a sink"
+	core.Sink([]interface{}{s})           // want "a source has reached a sink"
+	core.Sink([]interface{}{0, "", s})    // want "a source has reached a sink"
+	core.Sink([]interface{}{0, "", s}...) // want "a source has reached a sink"
+}
diff --git a/internal/testdata/src/example.com/tests/propagation/tests.go b/internal/testdata/src/example.com/tests/propagation/tests.go
new file mode 100644
index 0000000..435464c
--- /dev/null
+++ b/internal/testdata/src/example.com/tests/propagation/tests.go
@@ -0,0 +1,40 @@
+// 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 (
+	"fmt"
+
+	"example.com/core"
+)
+
+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"
+}
diff --git a/internal/testdata/src/example.com/tests/sinks/tests.go b/internal/testdata/src/example.com/tests/sinks/tests.go
index 0c753c2..18f30f1 100644
--- a/internal/testdata/src/example.com/tests/sinks/tests.go
+++ b/internal/testdata/src/example.com/tests/sinks/tests.go
@@ -25,9 +25,6 @@ func TestSinks(s core.Source, writer io.Writer) {
 	core.Sinkf("a source: %v", s) // want "a source has reached a sink"
 	core.FSinkf(writer, s)        // want "a source has reached a sink"
 	core.OneArgSink(s)            // want "a source has reached a sink"
-
-	core.Sink([]interface{}{s, s, s}...) // want "a source has reached a sink"
-	core.Sink([]interface{}{s, s, s})    // TODO want "a source has reached a sink"
 }
 
 func TestSinksWithRef(s *core.Source, writer io.Writer) {
@@ -35,9 +32,6 @@ func TestSinksWithRef(s *core.Source, writer io.Writer) {
 	core.Sinkf("a source: %v", s) // want "a source has reached a sink"
 	core.FSinkf(writer, s)        // want "a source has reached a sink"
 	core.OneArgSink(s)            // want "a source has reached a sink"
-
-	core.Sink([]interface{}{s, s, s}...) // want "a source has reached a sink"
-	core.Sink([]interface{}{s, s, s})    // TODO want "a source has reached a sink"
 }
 
 func TestSinksInnocuous(innoc core.Innocuous, writer io.Writer) {
diff --git a/internal/testdata/test-config.json b/internal/testdata/test-config.json
index 67e634c..340cb40 100644
--- a/internal/testdata/test-config.json
+++ b/internal/testdata/test-config.json
@@ -9,7 +9,7 @@
   "Sinks": [
     {
       "PackageRE": "^example.com/core$",
-      "MethodRE": "Sink"
+      "MethodRE": "Sinkf?$"
     }
   ],
   "Sanitizers": [