message/pipeline: use ssa and callgraph

instead of naively traversing the AST.

Change-Id: I0657e1c4fff4f7849e6d30098c99e8677ecd3de0
Reviewed-on: https://go-review.googlesource.com/102716
Run-TryBot: Marcel van Lohuizen <mpvl@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Ross Light <light@google.com>
diff --git a/message/pipeline/extract.go b/message/pipeline/extract.go
index 9be2bac..e006216 100644
--- a/message/pipeline/extract.go
+++ b/message/pipeline/extract.go
@@ -12,16 +12,21 @@
 	"go/format"
 	"go/token"
 	"go/types"
-	"path"
 	"path/filepath"
 	"strings"
 	"unicode"
 	"unicode/utf8"
 
 	fmtparser "golang.org/x/text/internal/format"
+	"golang.org/x/tools/go/callgraph"
+	"golang.org/x/tools/go/callgraph/cha"
 	"golang.org/x/tools/go/loader"
+	"golang.org/x/tools/go/ssa"
+	"golang.org/x/tools/go/ssa/ssautil"
 )
 
+const debug = false
+
 // TODO:
 // - merge information into existing files
 // - handle different file formats (PO, XLIFF)
@@ -39,6 +44,7 @@
 		return nil, wrap(err, "")
 	}
 
+	x.seedEndpoints()
 	x.extractMessages()
 
 	return &State{
@@ -52,23 +58,296 @@
 }
 
 type extracter struct {
-	conf     loader.Config
-	iprog    *loader.Program
+	conf      loader.Config
+	iprog     *loader.Program
+	prog      *ssa.Program
+	callGraph *callgraph.Graph
+
+	// Calls and other expressions to collect.
+	exprs    map[token.Pos]ast.Expr
+	funcs    map[token.Pos]*callData
 	messages []Message
 }
 
 func newExtracter(c *Config) (x *extracter, err error) {
 	x = &extracter{
-		conf: loader.Config{},
+		conf:  loader.Config{},
+		exprs: map[token.Pos]ast.Expr{},
+		funcs: map[token.Pos]*callData{},
 	}
 
 	x.iprog, err = loadPackages(&x.conf, c.Packages)
 	if err != nil {
 		return nil, wrap(err, "")
 	}
+
+	x.prog = ssautil.CreateProgram(x.iprog, 0)
+	x.prog.Build()
+
+	x.callGraph = cha.CallGraph(x.prog)
+
 	return x, nil
 }
 
+func (x *extracter) seedEndpoints() {
+	pkg := x.prog.Package(x.iprog.Package("golang.org/x/text/message").Pkg)
+	typ := types.NewPointer(pkg.Type("Printer").Type())
+
+	x.handleFunc(x.prog.LookupMethod(typ, pkg.Pkg, "Printf"), &callData{
+		formatPos: 1,
+		argPos:    2,
+		isMethod:  true,
+	})
+	x.handleFunc(x.prog.LookupMethod(typ, pkg.Pkg, "Sprintf"), &callData{
+		formatPos: 1,
+		argPos:    2,
+		isMethod:  true,
+	})
+	x.handleFunc(x.prog.LookupMethod(typ, pkg.Pkg, "Fprintf"), &callData{
+		formatPos: 2,
+		argPos:    3,
+		isMethod:  true,
+	})
+}
+
+type callData struct {
+	call    ssa.CallInstruction
+	formats []constant.Value
+
+	callee    *callData
+	isMethod  bool
+	formatPos int
+	argPos    int   // varargs at this position in the call
+	argTypes  []int // arguments extractable from this position
+}
+
+func (c *callData) callFormatPos() int {
+	c = c.callee
+	if c.isMethod {
+		return c.formatPos - 1
+	}
+	return c.formatPos
+}
+
+func (c *callData) callArgsStart() int {
+	c = c.callee
+	if c.isMethod {
+		return c.argPos - 1
+	}
+	return c.argPos
+}
+
+func (c *callData) Pos() token.Pos      { return c.call.Pos() }
+func (c *callData) Pkg() *types.Package { return c.call.Parent().Pkg.Pkg }
+
+func (x *extracter) handleFunc(f *ssa.Function, fd *callData) {
+	for _, e := range x.callGraph.Nodes[f].In {
+		if e.Pos() == 0 {
+			continue
+		}
+
+		call := e.Site
+		caller := x.funcs[call.Pos()]
+		if caller != nil {
+			// TODO: theoretically a format string could be passed to multiple
+			// arguments of a function. Support this eventually.
+			continue
+		}
+		x.debug(call, "CALL", f.String())
+
+		caller = &callData{
+			call:      call,
+			callee:    fd,
+			formatPos: -1,
+			argPos:    -1,
+		}
+		// Offset by one if we are invoking an interface method.
+		offset := 0
+		if call.Common().IsInvoke() {
+			offset = -1
+		}
+		x.funcs[call.Pos()] = caller
+		if fd.argPos >= 0 {
+			x.visitArgs(caller, call.Common().Args[fd.argPos+offset])
+		}
+		x.visitFormats(caller, call.Common().Args[fd.formatPos+offset])
+	}
+}
+
+type posser interface {
+	Pos() token.Pos
+	Parent() *ssa.Function
+}
+
+func (x *extracter) debug(v posser, header string, args ...interface{}) {
+	if debug {
+		pos := ""
+		if p := v.Parent(); p != nil {
+			pos = posString(&x.conf, p.Package().Pkg, v.Pos())
+		}
+		if header != "CALL" && header != "INSERT" {
+			header = "  " + header
+		}
+		fmt.Printf("%-32s%-10s%-15T ", pos+fmt.Sprintf("@%d", v.Pos()), header, v)
+		for _, a := range args {
+			fmt.Printf(" %v", a)
+		}
+		fmt.Println()
+	}
+}
+
+// visitFormats finds the original source of the value. The returned index is
+// position of the argument if originated from a function argument or -1
+// otherwise.
+func (x *extracter) visitFormats(call *callData, v ssa.Value) {
+	if v == nil {
+		return
+	}
+	x.debug(v, "VALUE", v)
+
+	switch v := v.(type) {
+	case *ssa.Phi:
+		for _, e := range v.Edges {
+			x.visitFormats(call, e)
+		}
+
+	case *ssa.Const:
+		// Only record strings with letters.
+		if isMsg(constant.StringVal(v.Value)) {
+			x.debug(call.call, "FORMAT", v.Value.ExactString())
+			call.formats = append(call.formats, v.Value)
+		}
+		// TODO: handle %m-directive.
+
+	case *ssa.Global:
+		// TODO: record value if a string and try to determine a possible
+		// constant value from the ast data.
+
+	case *ssa.FieldAddr, *ssa.Field:
+		// TODO: mark field index v.Field of v.X.Type() for extraction. extract
+		// an example args as to give parameters for the translator.
+
+	case *ssa.Slice:
+		if v.Low == nil && v.High == nil && v.Max == nil {
+			x.visitFormats(call, v.X)
+		}
+
+	case *ssa.Parameter:
+		// TODO: handle the function for the index parameter.
+		f := v.Parent()
+		for i, p := range f.Params {
+			if p == v {
+				if call.formatPos < 0 {
+					call.formatPos = i
+					// TODO: is there a better way to detect this is calling
+					// a method rather than a function?
+					call.isMethod = len(f.Params) > f.Signature.Params().Len()
+					x.handleFunc(v.Parent(), call)
+				} else if debug && i != call.formatPos {
+					// TODO: support this.
+					fmt.Printf("WARNING:%s: format string passed to arg %d and %d\n",
+						posString(&x.conf, call.Pkg(), call.Pos()),
+						call.formatPos, i)
+				}
+			}
+		}
+
+	case *ssa.Alloc:
+		if ref := v.Referrers(); ref == nil {
+			for _, r := range *ref {
+				values := []ssa.Value{}
+				for _, o := range r.Operands(nil) {
+					if o == nil || *o == v {
+						continue
+					}
+					values = append(values, *o)
+				}
+				// TODO: return something different if we care about multiple
+				// values as well.
+				if len(values) == 1 {
+					x.visitFormats(call, values[0])
+				}
+			}
+		}
+
+		// TODO:
+	// case *ssa.Index:
+	// 	// Get all values in the array if applicable
+	// case *ssa.IndexAddr:
+	// 	// Get all values in the slice or *array if applicable.
+	// case *ssa.Lookup:
+	// 	// Get all values in the map if applicable.
+
+	case *ssa.FreeVar:
+		// TODO: find the link between free variables and parameters:
+		//
+		// func freeVar(p *message.Printer, str string) {
+		// 	fn := func(p *message.Printer) {
+		// 		p.Printf(str)
+		// 	}
+		// 	fn(p)
+		// }
+
+	case ssa.Instruction:
+		rands := v.Operands(nil)
+		if len(rands) == 1 && rands[0] != nil {
+			x.visitFormats(call, *rands[0])
+		}
+	case *ssa.Call:
+	}
+}
+
+// Note: a function may have an argument marked as both format and passthrough.
+
+// visitArgs collects information on arguments. For wrapped functions it will
+// just determine the position of the variable args slice.
+func (x *extracter) visitArgs(fd *callData, v ssa.Value) {
+	if v == nil {
+		return
+	}
+	x.debug(v, "ARGV", v)
+	switch v := v.(type) {
+
+	case *ssa.Slice:
+		if v.Low == nil && v.High == nil && v.Max == nil {
+			x.visitArgs(fd, v.X)
+		}
+
+	case *ssa.Parameter:
+		// TODO: handle the function for the index parameter.
+		f := v.Parent()
+		for i, p := range f.Params {
+			if p == v {
+				fd.argPos = i
+			}
+		}
+
+	case *ssa.Alloc:
+		if ref := v.Referrers(); ref == nil {
+			for _, r := range *ref {
+				values := []ssa.Value{}
+				for _, o := range r.Operands(nil) {
+					if o == nil || *o == v {
+						continue
+					}
+					values = append(values, *o)
+				}
+				// TODO: return something different if we care about
+				// multiple values as well.
+				if len(values) == 1 {
+					x.visitArgs(fd, values[0])
+				}
+			}
+		}
+
+	case ssa.Instruction:
+		rands := v.Operands(nil)
+		if len(rands) == 1 && rands[0] != nil {
+			x.visitArgs(fd, *rands[0])
+		}
+	}
+}
+
 func (x *extracter) extractMessages() {
 	// print returns Go syntax for the specified node.
 	print := func(n ast.Node) string {
@@ -95,46 +374,21 @@
 				if !ok {
 					return true
 				}
+				data := x.funcs[call.Lparen]
+				if data == nil || len(data.formats) == 0 {
+					return true
+				}
+				x.debug(data.call, "INSERT", data.formats)
 
-				// Skip calls of functions other than
-				// (*message.Printer).{Sp,Fp,P}rintf.
-				sel, ok := call.Fun.(*ast.SelectorExpr)
-				if !ok {
-					return true
-				}
-				meth := info.Selections[sel]
-				if meth == nil || meth.Kind() != types.MethodVal {
-					return true
-				}
-				// TODO: remove cheap hack and check if the type either
-				// implements some interface or is specifically of type
-				// "golang.org/x/text/message".Printer.
-				m, ok := extractFuncs[path.Base(meth.Recv().String())]
-				if !ok {
-					return true
-				}
-
-				fmtType, ok := m[meth.Obj().Name()]
-				if !ok {
-					return true
-				}
-				// argn is the index of the format string.
-				argn := fmtType.arg
+				argn := data.callFormatPos()
 				if argn >= len(call.Args) {
 					return true
 				}
+				format := call.Args[argn]
 
-				args := call.Args[fmtType.arg:]
-
-				fmtMsg, ok := msgStr(info, args[0])
-				if !ok {
-					// TODO: identify the type of the format argument. If it
-					// is not a string, multiple keys may be defined.
-					return true
-				}
 				comment := ""
 				key := []string{}
-				if ident, ok := args[0].(*ast.Ident); ok {
+				if ident, ok := format.(*ast.Ident); ok {
 					key = append(key, ident.Name)
 					if v, ok := ident.Obj.Decl.(*ast.ValueSpec); ok && v.Comment != nil {
 						// TODO: get comment above ValueSpec as well
@@ -143,36 +397,40 @@
 				}
 
 				arguments := []argument{}
-				args = args[1:]
-				simArgs := make([]interface{}, len(args))
-				for i, arg := range args {
-					expr := print(arg)
-					val := ""
-					if v := info.Types[arg].Value; v != nil {
-						val = v.ExactString()
-						simArgs[i] = val
-						switch arg.(type) {
-						case *ast.BinaryExpr, *ast.UnaryExpr:
-							expr = val
+				simArgs := []interface{}{}
+				if data.callArgsStart() >= 0 {
+					args := call.Args[data.callArgsStart():]
+					simArgs = make([]interface{}, len(args))
+					for i, arg := range args {
+						expr := print(arg)
+						val := ""
+						if v := info.Types[arg].Value; v != nil {
+							val = v.ExactString()
+							simArgs[i] = val
+							switch arg.(type) {
+							case *ast.BinaryExpr, *ast.UnaryExpr:
+								expr = val
+							}
 						}
+						arguments = append(arguments, argument{
+							ArgNum:         i + 1,
+							Type:           info.Types[arg].Type.String(),
+							UnderlyingType: info.Types[arg].Type.Underlying().String(),
+							Expr:           expr,
+							Value:          val,
+							Comment:        getComment(arg),
+							Position:       posString(&x.conf, info.Pkg, arg.Pos()),
+							// TODO report whether it implements
+							// interfaces plural.Interface,
+							// gender.Interface.
+						})
 					}
-					arguments = append(arguments, argument{
-						ArgNum:         i + 1,
-						Type:           info.Types[arg].Type.String(),
-						UnderlyingType: info.Types[arg].Type.Underlying().String(),
-						Expr:           expr,
-						Value:          val,
-						Comment:        getComment(arg),
-						Position:       posString(&x.conf, info.Pkg, arg.Pos()),
-						// TODO report whether it implements
-						// interfaces plural.Interface,
-						// gender.Interface.
-					})
 				}
 
-				formats := []string{fmtMsg}
+				formats := data.formats
 				for _, c := range formats {
-					fmtMsg = c
+					key := append([]string{}, key...)
+					fmtMsg := constant.StringVal(c)
 					msg := ""
 
 					ph := placeholders{index: map[string]string{}}
@@ -235,29 +493,6 @@
 	return filepath.Join(pkg.Path(), file)
 }
 
-// extractFuncs indicates the types and methods for which to extract strings,
-// and which argument to extract.
-// TODO: use the types in conf.Import("golang.org/x/text/message") to extract
-// the correct instances.
-var extractFuncs = map[string]map[string]extractType{
-	// TODO: Printer -> *golang.org/x/text/message.Printer
-	"message.Printer": {
-		"Printf":  extractType{arg: 0, format: true},
-		"Sprintf": extractType{arg: 0, format: true},
-		"Fprintf": extractType{arg: 1, format: true},
-
-		"Lookup": extractType{arg: 0},
-	},
-}
-
-type extractType struct {
-	// format indicates if the next arg is a formatted string or whether to
-	// concatenate all arguments
-	format bool
-	// arg indicates the position of the argument to extract.
-	arg int
-}
-
 func getID(arg *argument) string {
 	s := getLastComponent(arg.Expr)
 	s = strip(s)
@@ -322,17 +557,14 @@
 	return s[1+strings.LastIndexByte(s, '.'):]
 }
 
-func msgStr(info *loader.PackageInfo, e ast.Expr) (s string, ok bool) {
-	v := info.Types[e].Value
-	if v == nil || v.Kind() != constant.String {
-		return "", false
-	}
-	s = constant.StringVal(v)
-	// Only record strings with letters.
+// isMsg returns whether s should be translated.
+func isMsg(s string) bool {
+	// TODO: parse as format string and omit strings that contain letters
+	// coming from format verbs.
 	for _, r := range s {
 		if unicode.In(r, unicode.L) {
-			return s, true
+			return true
 		}
 	}
-	return "", false
+	return false
 }
diff --git a/message/pipeline/testdata/ssa/catalog_gen.go b/message/pipeline/testdata/ssa/catalog_gen.go
new file mode 100644
index 0000000..2e14d5a
--- /dev/null
+++ b/message/pipeline/testdata/ssa/catalog_gen.go
@@ -0,0 +1,37 @@
+// Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT.
+
+package main
+
+import (
+	"golang.org/x/text/language"
+	"golang.org/x/text/message"
+	"golang.org/x/text/message/catalog"
+)
+
+type dictionary struct {
+	index []uint32
+	data  string
+}
+
+func (d *dictionary) Lookup(key string) (data string, ok bool) {
+	p := messageKeyToIndex[key]
+	start, end := d.index[p], d.index[p+1]
+	if start == end {
+		return "", false
+	}
+	return d.data[start:end], true
+}
+
+func init() {
+	dict := map[string]catalog.Dictionary{}
+	fallback := language.MustParse("en-US")
+	cat, err := catalog.NewFromMap(dict, catalog.Fallback(fallback))
+	if err != nil {
+		panic(err)
+	}
+	message.DefaultCatalog = cat
+}
+
+var messageKeyToIndex = map[string]int{}
+
+// Total table size 0 bytes (0KiB); checksum: 811C9DC5
diff --git a/message/pipeline/testdata/ssa/extracted.gotext.json b/message/pipeline/testdata/ssa/extracted.gotext.json
new file mode 100644
index 0000000..cd24934
--- /dev/null
+++ b/message/pipeline/testdata/ssa/extracted.gotext.json
@@ -0,0 +1,229 @@
+{
+    "language": "en-US",
+    "messages": [
+        {
+            "id": "inline {ARG1}",
+            "key": "inline %s",
+            "message": "inline {ARG1}",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "ARG1",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "\"ARG1\""
+                }
+            ],
+            "position": "testdata/ssa/ssa.go:16:7"
+        },
+        {
+            "id": "global printer used {ARG1}",
+            "key": "global printer used %s",
+            "message": "global printer used {ARG1}",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "ARG1",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "\"ARG1\""
+                }
+            ],
+            "position": "testdata/ssa/ssa.go:17:8"
+        },
+        {
+            "id": "number: {2}, string: {STRING_ARG}, bool: {True}",
+            "key": "number: %d, string: %s, bool: %v",
+            "message": "number: {2}, string: {STRING_ARG}, bool: {True}",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "2",
+                    "string": "%[1]d",
+                    "type": "int",
+                    "underlyingType": "int",
+                    "argNum": 1,
+                    "expr": "2"
+                },
+                {
+                    "id": "STRING_ARG",
+                    "string": "%[2]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 2,
+                    "expr": "\"STRING ARG\""
+                },
+                {
+                    "id": "True",
+                    "string": "%[3]v",
+                    "type": "bool",
+                    "underlyingType": "bool",
+                    "argNum": 3,
+                    "expr": "true"
+                }
+            ],
+            "position": "testdata/ssa/ssa.go:20:9"
+        },
+        {
+            "id": "empty string",
+            "key": "empty string",
+            "message": "empty string",
+            "translation": "",
+            "position": "testdata/ssa/ssa.go:21:9"
+        },
+        {
+            "id": "Lovely weather today!",
+            "key": "Lovely weather today!",
+            "message": "Lovely weather today!",
+            "translation": "",
+            "position": "testdata/ssa/ssa.go:22:8"
+        },
+        {
+            "id": "number one",
+            "key": "number one",
+            "message": "number one",
+            "translation": "",
+            "position": "testdata/ssa/ssa.go:30:8"
+        },
+        {
+            "id": [
+                "v",
+                "number: {C}"
+            ],
+            "key": "number: %d",
+            "message": "number: {C}",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "C",
+                    "string": "%[1]d",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "c"
+                }
+            ],
+            "position": "testdata/ssa/ssa.go:77:10"
+        },
+        {
+            "id": [
+                "format",
+                "constant local {Args}"
+            ],
+            "key": "constant local %s",
+            "message": "constant local {Args}",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Args",
+                    "string": "%[1]s",
+                    "type": "[]interface{}",
+                    "underlyingType": "[]interface{}",
+                    "argNum": 1,
+                    "expr": "args"
+                }
+            ],
+            "position": "testdata/ssa/ssa.go:86:11"
+        },
+        {
+            "id": [
+                "a",
+                "foo {Arg1} {B}"
+            ],
+            "key": "foo %s %s",
+            "message": "foo {Arg1} {B}",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Arg1",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "arg1"
+                },
+                {
+                    "id": "B",
+                    "string": "%[2]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 2,
+                    "expr": "b"
+                }
+            ],
+            "position": "testdata/ssa/ssa.go:137:7"
+        },
+        {
+            "id": [
+                "a",
+                "bar {Arg1} {B}"
+            ],
+            "key": "bar %s %s",
+            "message": "bar {Arg1} {B}",
+            "translation": "",
+            "placeholders": [
+                {
+                    "id": "Arg1",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "arg1"
+                },
+                {
+                    "id": "B",
+                    "string": "%[2]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 2,
+                    "expr": "b"
+                }
+            ],
+            "position": "testdata/ssa/ssa.go:137:7"
+        },
+        {
+            "id": [
+                "a",
+                "foo"
+            ],
+            "key": "foo",
+            "message": "foo",
+            "translation": "",
+            "position": "testdata/ssa/ssa.go:151:8"
+        },
+        {
+            "id": [
+                "a",
+                "bar"
+            ],
+            "key": "bar",
+            "message": "bar",
+            "translation": "",
+            "position": "testdata/ssa/ssa.go:151:8"
+        },
+        {
+            "id": [
+                "a",
+                "baz"
+            ],
+            "key": "baz",
+            "message": "baz",
+            "translation": "",
+            "position": "testdata/ssa/ssa.go:151:8"
+        },
+        {
+            "id": [
+                "str",
+                "const str"
+            ],
+            "key": "const str",
+            "message": "const str",
+            "translation": "",
+            "position": "testdata/ssa/ssa.go:165:11"
+        }
+    ]
+}
\ No newline at end of file
diff --git a/message/pipeline/testdata/ssa/ssa.go b/message/pipeline/testdata/ssa/ssa.go
new file mode 100644
index 0000000..aae2e8d
--- /dev/null
+++ b/message/pipeline/testdata/ssa/ssa.go
@@ -0,0 +1,175 @@
+package main
+
+import (
+	"golang.org/x/text/language"
+	"golang.org/x/text/message"
+)
+
+// In this test, lowercap strings are ones that need to be picked up for
+// translation, whereas uppercap strings should not be picked up.
+
+func main() {
+	p := message.NewPrinter(language.English)
+
+	// TODO: probably should use type instead of string content for argument
+	// substitution.
+	wrapf(p, "inline %s", "ARG1")
+	gwrapf("global printer used %s", "ARG1")
+
+	w := wrapped{p}
+	w.wrapf("number: %d, string: %s, bool: %v", 2, "STRING ARG", true)
+	w.wrapf("empty string")
+	w.wrap("Lovely weather today!")
+
+	more(&w)
+}
+
+var printer = message.NewPrinter(language.English)
+
+func more(w wrapper) {
+	w.wrap("number one")
+	w.wrapf("speed of light: %s", "C")
+}
+
+func gwrapf(format string, args ...interface{}) {
+	v := format
+	a := args
+	printer.Printf(v, a...)
+}
+
+func wrapf(p *message.Printer, format string, args ...interface{}) {
+	v := format
+	a := args
+	p.Printf(v, a...)
+}
+
+func wrap(p *message.Printer, format string) {
+	v := format
+	b := "0"
+	a := []interface{}{3, b}
+	s := a[:]
+	p.Printf(v, s...)
+}
+
+type wrapper interface {
+	wrapf(format string, args ...interface{})
+	wrap(msg string)
+}
+
+type wrapped struct {
+	p *message.Printer
+}
+
+// TODO: calls over interfaces do not get picked up. It looks like this is
+// because w is not a pointer receiver, while the other method is. Mixing of
+// receiver types does not seem to be allowed by callgraph/cha.
+func (w wrapped) wrapf(format string, args ...interface{}) {
+	w.p.Printf(format, args...)
+}
+
+func (w *wrapped) wrap(msg string) {
+	w.p.Printf(msg)
+}
+
+func fint(p *message.Printer, x int) {
+	v := "number: %d"
+	const c = "DAFDA"
+	p.Printf(v, c)
+}
+
+const format = "constant local" + " %s"
+
+// NOTE: pass is not called. Ensure it is picked up anyway.
+func pass(p *message.Printer, args ...interface{}) {
+	// TODO: find an example caller to find substituted types and argument
+	// examples.
+	p.Sprintf(format, args...)
+}
+
+func lookup(p *message.Printer, x int) {
+	// TODO: pick up all elements from slice foo.
+	p.Printf(foo[x])
+}
+
+var foo = []string{
+	"aaaa",
+	"bbbb",
+}
+
+func field(p *message.Printer, x int) {
+	// TODO: pick up strings in field BAR from all composite literals of
+	// typeof(strct.Foo.Bar).
+	p.Printf(strct.Foo.Bar, x)
+}
+
+type fooStruct struct {
+	Foo barStruct
+}
+
+type barStruct struct {
+	other int
+	Bar   string
+}
+
+var strct = fooStruct{
+	Foo: barStruct{0, "foo %d"},
+}
+
+func call(p *message.Printer, x int) {
+	// TODO: pick up constant return values.
+	p.Printf(fn())
+}
+
+func fn() string {
+	return "const str"
+}
+
+// Both strings get picked up.
+func ifConst(p *message.Printer, cond bool, arg1 string) {
+	a := "foo %s %s"
+	if cond {
+		a = "bar %s %s"
+	}
+	b := "FOO"
+	if cond {
+		b = "BAR"
+	}
+	wrapf(p, a, arg1, b)
+}
+
+// Pick up all non-empty strings in this function.
+func ifConst2(x int) {
+	a := ""
+	switch x {
+	case 0:
+		a = "foo"
+	case 1:
+		a = "bar"
+	case 2:
+		a = "baz"
+	}
+	gwrapf(a)
+}
+
+// TODO: pick up strings passed to the second argument in calls to freeVar.
+func freeVar(p *message.Printer, str string) {
+	fn := func(p *message.Printer) {
+		p.Printf(str)
+	}
+	fn(p)
+}
+
+func freeConst(p *message.Printer) {
+	const str = "const str"
+	fn := func(p *message.Printer) {
+		p.Printf(str)
+	}
+	fn(p)
+}
+
+func global(p *message.Printer) {
+	// TODO: pick up evaluations of globals to string.
+	p.Printf(globalStr)
+}
+
+var globalStr = "global string"