+++ /dev/null
-// 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
-// map of valid formats (knownFormats). This map can be used to
-// automatically rewrite format strings across all compiler
-// files with the -r flag.
-//
-// The format map 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"). To update the map auto-
-// matically from the compiler source's use of format strings,
-// use the -u flag. (Whether formats are valid for the values
-// to be formatted must be verified manually, of course.)
-//
-// The -v flag prints out the names of all functions called
-// with a format string, the names of files that were not
-// processed, and any format rewrites made (with -r).
-//
-// Run as: go test -run Formats [-r][-u][-v]
-//
-// Known shortcomings:
-// - 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"
- "io/fs"
- "io/ioutil"
- "log"
- "os"
- "path/filepath"
- "sort"
- "strconv"
- "strings"
- "testing"
- "unicode/utf8"
-)
-
-var (
- rewrite = flag.Bool("r", false, "rewrite format strings")
- update = flag.Bool("u", false, "update known formats")
-)
-
-// 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) {
- if testing.Short() && testenv.Builder() == "" {
- t.Skip("Skipping in short mode")
- }
- testenv.MustHaveGoBuild(t) // more restrictive than necessary, but that's ok
-
- // process all directories
- filepath.WalkDir(".", func(path string, info fs.DirEntry, err error) error {
- if info.IsDir() {
- if info.Name() == "testdata" {
- return filepath.SkipDir
- }
-
- importPath := filepath.Join("cmd/compile", path)
- if ignoredPackages[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"
-
- // Do not bother reporting basic types, nor %v, %T, %p.
- // Vet handles basic types, and those three formats apply to all types.
- if !strings.Contains(typ, ".") || (in == "%v" || in == "%T" || in == "%p") {
- return in
- }
-
- // 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 && *rewrite {
- for _, file := range updatedFiles {
- var buf bytes.Buffer
- if err := format.Node(&buf, fset, file.ast); err != nil {
- t.Errorf("WARNING: gofmt %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 the names of all functions called with 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 called with a format string")
- writeList(os.Stdout, list)
- }
-
- // update formats
- if len(foundFormats) > 0 && *update {
- var list []string
- for s := range foundFormats {
- list = append(list, fmt.Sprintf("%q: \"\",", s))
- }
- var buf bytes.Buffer
- buf.WriteString(knownFormatsHeader)
- writeList(&buf, list)
- buf.WriteString("}\n")
- out, err := format.Source(buf.Bytes())
- const outfile = "fmtmap_test.go"
- if err != nil {
- t.Errorf("WARNING: gofmt %s failed: %v", outfile, err)
- out = buf.Bytes() // continue with unformatted source
- }
- if err = ioutil.WriteFile(outfile, out, 0644); err != nil {
- t.Errorf("WARNING: updating format map failed: %v", err)
- }
- }
-
- // check that knownFormats is up to date
- if !*rewrite && !*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("format map is out of date; run 'go test -u' to update and manually verify correctness of change'")
- }
- }
-
- // 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")
- writeList(os.Stdout, 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 {
- if ignoredFunctions[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 (ignore %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
- })
- }
-}
-
-// writeList writes list in sorted order to w.
-func writeList(w io.Writer, list []string) {
- sort.Strings(list)
- for _, s := range list {
- fmt.Fprintln(w, "\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 {
- s := filepath.ToSlash(typ.String())
-
- // Report all the concrete IR types as Node, to shorten fmtmap.
- const ir = "cmd/compile/internal/ir."
- if s == "*"+ir+"Name" || s == "*"+ir+"Func" || s == "*"+ir+"Decl" ||
- s == ir+"Ntype" || s == ir+"Expr" || s == ir+"Stmt" ||
- strings.HasPrefix(s, "*"+ir) && (strings.HasSuffix(s, "Expr") || strings.HasSuffix(s, "Stmt")) {
- return "cmd/compile/internal/ir.Node"
- }
-
- return s
-}
-
-// 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:]...))
-}
-
-// ignoredPackages is the set of packages which can
-// be ignored.
-var ignoredPackages = map[string]bool{}
-
-// ignoredFunctions is the set of functions which may have
-// format-like arguments but which don't do any formatting and
-// thus may be ignored.
-var ignoredFunctions = 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 map)
- 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)
- }
- }
-}
-
-const knownFormatsHeader = `// Copyright 2018 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 the knownFormats map which records the valid
-// formats for a given type. The valid formats must correspond to
-// supported compiler formats implemented in fmt.go, or whatever
-// other format verbs are implemented for the given type. The map may
-// also be used to change the use of a format verb across all compiler
-// sources automatically (for instance, if the implementation of fmt.go
-// changes), by using the -r option together with the new formats in the
-// map. To generate this file automatically from the existing source,
-// run: go test -run Formats -u.
-//
-// See the package comment in fmt_test.go for additional information.
-
-package main_test
-
-// 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.
-var knownFormats = map[string]string{
-`
+++ /dev/null
-// Copyright 2018 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 the knownFormats map which records the valid
-// formats for a given type. The valid formats must correspond to
-// supported compiler formats implemented in fmt.go, or whatever
-// other format verbs are implemented for the given type. The map may
-// also be used to change the use of a format verb across all compiler
-// sources automatically (for instance, if the implementation of fmt.go
-// changes), by using the -r option together with the new formats in the
-// map. To generate this file automatically from the existing source,
-// run: go test -run Formats -u.
-//
-// See the package comment in fmt_test.go for additional information.
-
-package main_test
-
-// 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.
-var knownFormats = map[string]string{
- "*bytes.Buffer %s": "",
- "*cmd/compile/internal/ssa.Block %s": "",
- "*cmd/compile/internal/ssa.Func %s": "",
- "*cmd/compile/internal/ssa.Register %s": "",
- "*cmd/compile/internal/ssa.Value %s": "",
- "*cmd/compile/internal/types.Sym %+v": "",
- "*cmd/compile/internal/types.Sym %S": "",
- "*cmd/compile/internal/types.Type %+v": "",
- "*cmd/compile/internal/types.Type %-S": "",
- "*cmd/compile/internal/types.Type %L": "",
- "*cmd/compile/internal/types.Type %S": "",
- "*cmd/compile/internal/types.Type %s": "",
- "*math/big.Float %f": "",
- "*math/big.Int %s": "",
- "[]cmd/compile/internal/syntax.token %s": "",
- "cmd/compile/internal/arm.shift %d": "",
- "cmd/compile/internal/gc.RegIndex %d": "",
- "cmd/compile/internal/ir.Class %d": "",
- "cmd/compile/internal/ir.Node %+v": "",
- "cmd/compile/internal/ir.Node %L": "",
- "cmd/compile/internal/ir.Nodes %+v": "",
- "cmd/compile/internal/ir.Nodes %.v": "",
- "cmd/compile/internal/ir.Op %+v": "",
- "cmd/compile/internal/ssa.Aux %#v": "",
- "cmd/compile/internal/ssa.Aux %q": "",
- "cmd/compile/internal/ssa.Aux %s": "",
- "cmd/compile/internal/ssa.BranchPrediction %d": "",
- "cmd/compile/internal/ssa.ID %d": "",
- "cmd/compile/internal/ssa.LocalSlot %s": "",
- "cmd/compile/internal/ssa.Location %s": "",
- "cmd/compile/internal/ssa.Op %s": "",
- "cmd/compile/internal/ssa.ValAndOff %s": "",
- "cmd/compile/internal/ssa.flagConstant %s": "",
- "cmd/compile/internal/ssa.rbrank %d": "",
- "cmd/compile/internal/ssa.regMask %d": "",
- "cmd/compile/internal/ssa.register %d": "",
- "cmd/compile/internal/ssa.relation %s": "",
- "cmd/compile/internal/syntax.Error %q": "",
- "cmd/compile/internal/syntax.Expr %#v": "",
- "cmd/compile/internal/syntax.LitKind %d": "",
- "cmd/compile/internal/syntax.Operator %s": "",
- "cmd/compile/internal/syntax.Pos %s": "",
- "cmd/compile/internal/syntax.position %s": "",
- "cmd/compile/internal/syntax.token %q": "",
- "cmd/compile/internal/syntax.token %s": "",
- "cmd/compile/internal/types.Kind %d": "",
- "cmd/compile/internal/types.Kind %s": "",
- "cmd/compile/internal/walk.initKind %d": "",
- "go/constant.Value %#v": "",
- "math/big.Accuracy %s": "",
- "reflect.Type %s": "",
- "time.Duration %d": "",
-}