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"