// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// This file implements TestFormats; a test that verifies
// format strings in the compiler (this directory and all
// subdirectories, recursively).
//
// TestFormats finds potential (Printf, etc.) format strings.
// If they are used in a call, the format verbs are verified
// based on the matching argument type against a precomputed
// table of valid formats. The knownFormats table can be used
// to automatically rewrite format strings with the -u flag.
//
// A new knownFormats table based on the found formats is printed
// when the test is run in verbose mode (-v flag). The table
// needs to be updated whenever a new (type, format) combination
// is found and the format verb is not 'v' or 'T' (as in "%v" or
// "%T").
//
// Run as: go test -run Formats [-u][-v]
//
// Known bugs:
// - indexed format strings ("%[2]s", etc.) are not supported
// (the test will fail)
// - format strings that are not simple string literals cannot
// be updated automatically
// (the test will fail with respective warnings)
// - format strings in _test packages outside the current
// package are not processed
// (the test will report those files)
//
package main_test
import (
"bytes"
"flag"
"fmt"
"go/ast"
"go/build"
"go/constant"
"go/format"
"go/importer"
"go/parser"
"go/token"
"go/types"
"internal/testenv"
"io/ioutil"
"log"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"testing"
"unicode/utf8"
)
var update = flag.Bool("u", false, "update format strings")
// The following variables collect information across all processed files.
var (
fset = token.NewFileSet()
formatStrings = make(map[*ast.BasicLit]bool) // set of all potential format strings found
foundFormats = make(map[string]bool) // set of all formats found
callSites = make(map[*ast.CallExpr]*callSite) // map of all calls
)
// A File is a corresponding (filename, ast) pair.
type File struct {
name string
ast *ast.File
}
func TestFormats(t *testing.T) {
testenv.MustHaveGoBuild(t) // more restrictive than necessary, but that's ok
// process all directories
filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
if info.IsDir() {
if info.Name() == "testdata" {
return filepath.SkipDir
}
importPath := filepath.Join("cmd/compile", path)
if blacklistedPackages[filepath.ToSlash(importPath)] {
return filepath.SkipDir
}
pkg, err := build.Import(importPath, path, 0)
if err != nil {
if _, ok := err.(*build.NoGoError); ok {
return nil // nothing to do here
}
t.Fatal(err)
}
collectPkgFormats(t, pkg)
}
return nil
})
// test and rewrite formats
updatedFiles := make(map[string]File) // files that were rewritten
for _, p := range callSites {
// test current format literal and determine updated one
out := formatReplace(p.str, func(index int, in string) string {
if in == "*" {
return in // cannot rewrite '*' (as in "%*d")
}
// in != '*'
typ := p.types[index]
format := typ + " " + in // e.g., "*Node %n"
// check if format is known
out, known := knownFormats[format]
// record format if not yet found
_, found := foundFormats[format]
if !found {
foundFormats[format] = true
}
// report an error if the format is unknown and this is the first
// time we see it; ignore "%v" and "%T" which are always valid
if !known && !found && in != "%v" && in != "%T" {
t.Errorf("%s: unknown format %q for %s argument", posString(p.arg), in, typ)
}
if out == "" {
out = in
}
return out
})
// replace existing format literal if it changed
if out != p.str {
// we cannot replace the argument if it's not a string literal for now
// (e.g., it may be "foo" + "bar")
lit, ok := p.arg.(*ast.BasicLit)
if !ok {
delete(callSites, p.call) // treat as if we hadn't found this site
continue
}
if testing.Verbose() {
fmt.Printf("%s:\n\t- %q\n\t+ %q\n", posString(p.arg), p.str, out)
}
// find argument index of format argument
index := -1
for i, arg := range p.call.Args {
if p.arg == arg {
index = i
break
}
}
if index < 0 {
// we may have processed the same call site twice,
// but that shouldn't happen
panic("internal error: matching argument not found")
}
// replace literal
new := *lit // make a copy
new.Value = strconv.Quote(out) // this may introduce "-quotes where there were `-quotes
p.call.Args[index] = &new
updatedFiles[p.file.name] = p.file
}
}
// write dirty files back
var filesUpdated bool
if len(updatedFiles) > 0 && *update {
for _, file := range updatedFiles {
var buf bytes.Buffer
if err := format.Node(&buf, fset, file.ast); err != nil {
t.Errorf("WARNING: formatting %s failed: %v", file.name, err)
continue
}
if err := ioutil.WriteFile(file.name, buf.Bytes(), 0x666); err != nil {
t.Errorf("WARNING: writing %s failed: %v", file.name, err)
continue
}
fmt.Printf("updated %s\n", file.name)
filesUpdated = true
}
}
// report all function names containing a format string
if len(callSites) > 0 && testing.Verbose() {
set := make(map[string]bool)
for _, p := range callSites {
set[nodeString(p.call.Fun)] = true
}
var list []string
for s := range set {
list = append(list, s)
}
fmt.Println("\nFunctions")
printList(list)
}
// report all formats found
if len(foundFormats) > 0 && testing.Verbose() {
var list []string
for s := range foundFormats {
list = append(list, fmt.Sprintf("%q: \"\",", s))
}
fmt.Println("\nvar knownFormats = map[string]string{")
printList(list)
fmt.Println("}")
}
// check that knownFormats is up to date
if !testing.Verbose() && !*update {
var mismatch bool
for s := range foundFormats {
if _, ok := knownFormats[s]; !ok {
mismatch = true
break
}
}
if !mismatch {
for s := range knownFormats {
if _, ok := foundFormats[s]; !ok {
mismatch = true
break
}
}
}
if mismatch {
t.Errorf("knownFormats is out of date; please 'go test -v fmt_test.go > foo', then extract new definition of knownFormats from foo")
}
}
// all format strings of calls must be in the formatStrings set (self-verification)
for _, p := range callSites {
if lit, ok := p.arg.(*ast.BasicLit); ok && lit.Kind == token.STRING {
if formatStrings[lit] {
// ok
delete(formatStrings, lit)
} else {
// this should never happen
panic(fmt.Sprintf("internal error: format string not found (%s)", posString(lit)))
}
}
}
// if we have any strings left, we may need to update them manually
if len(formatStrings) > 0 && filesUpdated {
var list []string
for lit := range formatStrings {
list = append(list, fmt.Sprintf("%s: %s", posString(lit), nodeString(lit)))
}
fmt.Println("\nWARNING: Potentially missed format strings")
printList(list)
t.Fail()
}
fmt.Println()
}
// A callSite describes a function call that appears to contain
// a format string.
type callSite struct {
file File
call *ast.CallExpr // call containing the format string
arg ast.Expr // format argument (string literal or constant)
str string // unquoted format string
types []string // argument types
}
func collectPkgFormats(t *testing.T, pkg *build.Package) {
// collect all files
var filenames []string
filenames = append(filenames, pkg.GoFiles...)
filenames = append(filenames, pkg.CgoFiles...)
filenames = append(filenames, pkg.TestGoFiles...)
// TODO(gri) verify _test files outside package
for _, name := range pkg.XTestGoFiles {
// don't process this test itself
if name != "fmt_test.go" && testing.Verbose() {
fmt.Printf("WARNING: %s not processed\n", filepath.Join(pkg.Dir, name))
}
}
// make filenames relative to .
for i, name := range filenames {
filenames[i] = filepath.Join(pkg.Dir, name)
}
// parse all files
files := make([]*ast.File, len(filenames))
for i, filename := range filenames {
f, err := parser.ParseFile(fset, filename, nil, parser.ParseComments)
if err != nil {
t.Fatal(err)
}
files[i] = f
}
// typecheck package
conf := types.Config{Importer: importer.Default()}
etypes := make(map[ast.Expr]types.TypeAndValue)
if _, err := conf.Check(pkg.ImportPath, fset, files, &types.Info{Types: etypes}); err != nil {
t.Fatal(err)
}
// collect all potential format strings (for extra verification later)
for _, file := range files {
ast.Inspect(file, func(n ast.Node) bool {
if s, ok := stringLit(n); ok && isFormat(s) {
formatStrings[n.(*ast.BasicLit)] = true
}
return true
})
}
// collect all formats/arguments of calls with format strings
for index, file := range files {
ast.Inspect(file, func(n ast.Node) bool {
if call, ok := n.(*ast.CallExpr); ok {
// ignore blacklisted functions
if blacklistedFunctions[nodeString(call.Fun)] {
return true
}
// look for an arguments that might be a format string
for i, arg := range call.Args {
if s, ok := stringVal(etypes[arg]); ok && isFormat(s) {
// make sure we have enough arguments
n := numFormatArgs(s)
if i+1+n > len(call.Args) {
t.Errorf("%s: not enough format args (blacklist %s?)", posString(call), nodeString(call.Fun))
break // ignore this call
}
// assume last n arguments are to be formatted;
// determine their types
argTypes := make([]string, n)
for i, arg := range call.Args[len(call.Args)-n:] {
if tv, ok := etypes[arg]; ok {
argTypes[i] = typeString(tv.Type)
}
}
// collect call site
if callSites[call] != nil {
panic("internal error: file processed twice?")
}
callSites[call] = &callSite{
file: File{filenames[index], file},
call: call,
arg: arg,
str: s,
types: argTypes,
}
break // at most one format per argument list
}
}
}
return true
})
}
}
// printList prints list in sorted order.
func printList(list []string) {
sort.Strings(list)
for _, s := range list {
fmt.Println("\t", s)
}
}
// posString returns a string representation of n's position
// in the form filename:line:col: .
func posString(n ast.Node) string {
if n == nil {
return ""
}
return fset.Position(n.Pos()).String()
}
// nodeString returns a string representation of n.
func nodeString(n ast.Node) string {
var buf bytes.Buffer
if err := format.Node(&buf, fset, n); err != nil {
log.Fatal(err) // should always succeed
}
return buf.String()
}
// typeString returns a string representation of n.
func typeString(typ types.Type) string {
return filepath.ToSlash(typ.String())
}
// stringLit returns the unquoted string value and true if
// n represents a string literal; otherwise it returns ""
// and false.
func stringLit(n ast.Node) (string, bool) {
if lit, ok := n.(*ast.BasicLit); ok && lit.Kind == token.STRING {
s, err := strconv.Unquote(lit.Value)
if err != nil {
log.Fatal(err) // should not happen with correct ASTs
}
return s, true
}
return "", false
}
// stringVal returns the (unquoted) string value and true if
// tv is a string constant; otherwise it returns "" and false.
func stringVal(tv types.TypeAndValue) (string, bool) {
if tv.IsValue() && tv.Value != nil && tv.Value.Kind() == constant.String {
return constant.StringVal(tv.Value), true
}
return "", false
}
// formatIter iterates through the string s in increasing
// index order and calls f for each format specifier '%..v'.
// The arguments for f describe the specifier's index range.
// If a format specifier contains a "*", f is called with
// the index range for "*" alone, before being called for
// the entire specifier. The result of f is the index of
// the rune at which iteration continues.
func formatIter(s string, f func(i, j int) int) {
i := 0 // index after current rune
var r rune // current rune
next := func() {
r1, w := utf8.DecodeRuneInString(s[i:])
if w == 0 {
r1 = -1 // signal end-of-string
}
r = r1
i += w
}
flags := func() {
for r == ' ' || r == '#' || r == '+' || r == '-' || r == '0' {
next()
}
}
index := func() {
if r == '[' {
log.Fatalf("cannot handle indexed arguments: %s", s)
}
}
digits := func() {
index()
if r == '*' {
i = f(i-1, i)
next()
return
}
for '0' <= r && r <= '9' {
next()
}
}
for next(); r >= 0; next() {
if r == '%' {
i0 := i
next()
flags()
digits()
if r == '.' {
next()
digits()
}
index()
// accept any letter (a-z, A-Z) as format verb;
// ignore anything else
if 'a' <= r && r <= 'z' || 'A' <= r && r <= 'Z' {
i = f(i0-1, i)
}
}
}
}
// isFormat reports whether s contains format specifiers.
func isFormat(s string) (yes bool) {
formatIter(s, func(i, j int) int {
yes = true
return len(s) // stop iteration
})
return
}
// oneFormat reports whether s is exactly one format specifier.
func oneFormat(s string) (yes bool) {
formatIter(s, func(i, j int) int {
yes = i == 0 && j == len(s)
return j
})
return
}
// numFormatArgs returns the number of format specifiers in s.
func numFormatArgs(s string) int {
count := 0
formatIter(s, func(i, j int) int {
count++
return j
})
return count
}
// formatReplace replaces the i'th format specifier s in the incoming
// string in with the result of f(i, s) and returns the new string.
func formatReplace(in string, f func(i int, s string) string) string {
var buf []byte
i0 := 0
index := 0
formatIter(in, func(i, j int) int {
if sub := in[i:j]; sub != "*" { // ignore calls for "*" width/length specifiers
buf = append(buf, in[i0:i]...)
buf = append(buf, f(index, sub)...)
i0 = j
}
index++
return j
})
return string(append(buf, in[i0:]...))
}
// blacklistedPackages is the set of packages which can
// be ignored.
var blacklistedPackages = map[string]bool{}
// blacklistedFunctions is the set of functions which may have
// format-like arguments but which don't do any formatting and
// thus may be ignored.
var blacklistedFunctions = map[string]bool{}
func init() {
// verify that knownFormats entries are correctly formatted
for key, val := range knownFormats {
// key must be "typename format", and format starts with a '%'
// (formats containing '*' alone are not collected in this table)
i := strings.Index(key, "%")
if i < 0 || !oneFormat(key[i:]) {
log.Fatalf("incorrect knownFormats key: %q", key)
}
// val must be "format" or ""
if val != "" && !oneFormat(val) {
log.Fatalf("incorrect knownFormats value: %q (key = %q)", val, key)
}
}
}
// knownFormats entries are of the form "typename format" -> "newformat".
// An absent entry means that the format is not recognized as valid.
// An empty new format means that the format should remain unchanged.
// To print out a new table, run: go test -run Formats -v.
var knownFormats = map[string]string{
"*bytes.Buffer %s": "",
"*cmd/compile/internal/gc.Mpflt %v": "",
"*cmd/compile/internal/gc.Mpint %v": "",
"*cmd/compile/internal/gc.Node %#v": "",
"*cmd/compile/internal/gc.Node %+S": "",
"*cmd/compile/internal/gc.Node %+v": "",
"*cmd/compile/internal/gc.Node %0j": "",
"*cmd/compile/internal/gc.Node %L": "",
"*cmd/compile/internal/gc.Node %S": "",
"*cmd/compile/internal/gc.Node %j": "",
"*cmd/compile/internal/gc.Node %p": "",
"*cmd/compile/internal/gc.Node %v": "",
"*cmd/compile/internal/ssa.Block %s": "",
"*cmd/compile/internal/ssa.Block %v": "",
"*cmd/compile/internal/ssa.Func %s": "",
"*cmd/compile/internal/ssa.Func %v": "",
"*cmd/compile/internal/ssa.LocalSlot %+v": "",
"*cmd/compile/internal/ssa.LocalSlot %v": "",
"*cmd/compile/internal/ssa.Register %s": "",
"*cmd/compile/internal/ssa.SparseTreeNode %v": "",
"*cmd/compile/internal/ssa.Value %s": "",
"*cmd/compile/internal/ssa.Value %v": "",
"*cmd/compile/internal/ssa.VarLoc %v": "",
"*cmd/compile/internal/ssa.sparseTreeMapEntry %v": "",
"*cmd/compile/internal/types.Field %p": "",
"*cmd/compile/internal/types.Field %v": "",
"*cmd/compile/internal/types.Sym %+v": "",
"*cmd/compile/internal/types.Sym %-v": "",
"*cmd/compile/internal/types.Sym %0S": "",
"*cmd/compile/internal/types.Sym %S": "",
"*cmd/compile/internal/types.Sym %p": "",
"*cmd/compile/internal/types.Sym %v": "",
"*cmd/compile/internal/types.Type %#v": "",
"*cmd/compile/internal/types.Type %+v": "",
"*cmd/compile/internal/types.Type %-S": "",
"*cmd/compile/internal/types.Type %0S": "",
"*cmd/compile/internal/types.Type %L": "",
"*cmd/compile/internal/types.Type %S": "",
"*cmd/compile/internal/types.Type %p": "",
"*cmd/compile/internal/types.Type %s": "",
"*cmd/compile/internal/types.Type %v": "",
"*cmd/internal/dwarf.Location %#v": "",
"*cmd/internal/obj.Addr %v": "",
"*cmd/internal/obj.LSym %v": "",
"*math/big.Int %#x": "",
"*math/big.Int %s": "",
"[16]byte %x": "",
"[]*cmd/compile/internal/gc.Node %v": "",
"[]*cmd/compile/internal/gc.Sig %#v": "",
"[]*cmd/compile/internal/ssa.Value %v": "",
"[][]cmd/compile/internal/ssa.SlotID %v": "",
"[]byte %s": "",
"[]byte %x": "",
"[]cmd/compile/internal/ssa.Edge %v": "",
"[]cmd/compile/internal/ssa.ID %v": "",
"[]cmd/compile/internal/ssa.VarLocList %v": "",
"[]cmd/compile/internal/syntax.token %s": "",
"[]string %v": "",
"bool %v": "",
"byte %08b": "",
"byte %c": "",
"cmd/compile/internal/arm.shift %d": "",
"cmd/compile/internal/gc.Class %d": "",
"cmd/compile/internal/gc.Class %s": "",
"cmd/compile/internal/gc.Class %v": "",
"cmd/compile/internal/gc.Ctype %d": "",
"cmd/compile/internal/gc.Ctype %v": "",
"cmd/compile/internal/gc.Level %d": "",
"cmd/compile/internal/gc.Level %v": "",
"cmd/compile/internal/gc.Nodes %#v": "",
"cmd/compile/internal/gc.Nodes %+v": "",
"cmd/compile/internal/gc.Nodes %.v": "",
"cmd/compile/internal/gc.Nodes %v": "",
"cmd/compile/internal/gc.Op %#v": "",
"cmd/compile/internal/gc.Op %d": "",
"cmd/compile/internal/gc.Op %v": "",
"cmd/compile/internal/gc.Val %#v": "",
"cmd/compile/internal/gc.Val %T": "",
"cmd/compile/internal/gc.Val %v": "",
"cmd/compile/internal/gc.fmtMode %d": "",
"cmd/compile/internal/gc.initKind %d": "",
"cmd/compile/internal/gc.locID %v": "",
"cmd/compile/internal/ssa.BranchPrediction %d": "",
"cmd/compile/internal/ssa.Edge %v": "",
"cmd/compile/internal/ssa.GCNode %v": "",
"cmd/compile/internal/ssa.ID %d": "",
"cmd/compile/internal/ssa.ID %v": "",
"cmd/compile/internal/ssa.LocalSlot %s": "",
"cmd/compile/internal/ssa.Location %s": "",
"cmd/compile/internal/ssa.Op %s": "",
"cmd/compile/internal/ssa.Op %v": "",
"cmd/compile/internal/ssa.ValAndOff %s": "",
"cmd/compile/internal/ssa.VarLocList %v": "",
"cmd/compile/internal/ssa.rbrank %d": "",
"cmd/compile/internal/ssa.regMask %d": "",
"cmd/compile/internal/ssa.register %d": "",
"cmd/compile/internal/syntax.Expr %#v": "",
"cmd/compile/internal/syntax.Node %T": "",
"cmd/compile/internal/syntax.Operator %d": "",
"cmd/compile/internal/syntax.Operator %s": "",
"cmd/compile/internal/syntax.token %d": "",
"cmd/compile/internal/syntax.token %q": "",
"cmd/compile/internal/syntax.token %s": "",
"cmd/compile/internal/types.EType %d": "",
"cmd/compile/internal/types.EType %s": "",
"cmd/compile/internal/types.EType %v": "",
"cmd/internal/dwarf.Location %#v": "",
"cmd/internal/src.Pos %s": "",
"cmd/internal/src.Pos %v": "",
"error %v": "",
"float64 %.2f": "",
"float64 %.3f": "",
"float64 %.6g": "",
"float64 %g": "",
"int %-12d": "",
"int %-6d": "",
"int %-8o": "",
"int %02d": "",
"int %6d": "",
"int %c": "",
"int %d": "",
"int %v": "",
"int %x": "",
"int16 %d": "",
"int16 %x": "",
"int32 %d": "",
"int32 %v": "",
"int32 %x": "",
"int64 %+d": "",
"int64 %-10d": "",
"int64 %.5d": "",
"int64 %X": "",
"int64 %d": "",
"int64 %v": "",
"int64 %x": "",
"int8 %d": "",
"int8 %x": "",
"interface{} %#v": "",
"interface{} %T": "",
"interface{} %q": "",
"interface{} %s": "",
"interface{} %v": "",
"map[*cmd/compile/internal/gc.Node]*cmd/compile/internal/ssa.Value %v": "",
"reflect.Type %s": "",
"rune %#U": "",
"rune %c": "",
"string %-*s": "",
"string %-16s": "",
"string %-6s": "",
"string %.*s": "",
"string %q": "",
"string %s": "",
"string %v": "",
"time.Duration %d": "",
"time.Duration %v": "",
"uint %04x": "",
"uint %5d": "",
"uint %d": "",
"uint16 %d": "",
"uint16 %v": "",
"uint16 %x": "",
"uint32 %d": "",
"uint32 %x": "",
"uint64 %08x": "",
"uint64 %d": "",
"uint64 %x": "",
"uint8 %d": "",
"uint8 %x": "",
"uintptr %d": "",
}