blob: 7cbb0bdbf5fbd7aa0f25b76818a7e88042f55073 [file] [log] [blame]
// 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.
package printf
import (
"fmt"
"go/ast"
"go/types"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/internal/typeparams"
)
var errorType = types.Universe.Lookup("error").Type().Underlying().(*types.Interface)
// matchArgType reports an error if printf verb t is not appropriate for
// operand arg.
//
// If arg is a type parameter, the verb t must be appropriate for every type in
// the type parameter type set.
func matchArgType(pass *analysis.Pass, t printfArgType, arg ast.Expr) (reason string, ok bool) {
// %v, %T accept any argument type.
if t == anyType {
return "", true
}
typ := pass.TypesInfo.Types[arg].Type
if typ == nil {
return "", true // probably a type check problem
}
m := &argMatcher{t: t, seen: make(map[types.Type]bool)}
ok = m.match(typ, true)
return m.reason, ok
}
// argMatcher recursively matches types against the printfArgType t.
//
// To short-circuit recursion, it keeps track of types that have already been
// matched (or are in the process of being matched) via the seen map. Recursion
// arises from the compound types {map,chan,slice} which may be printed with %d
// etc. if that is appropriate for their element types, as well as from type
// parameters, which are expanded to the constituents of their type set.
//
// The reason field may be set to report the cause of the mismatch.
type argMatcher struct {
t printfArgType
seen map[types.Type]bool
reason string
}
// match checks if typ matches m's printf arg type. If topLevel is true, typ is
// the actual type of the printf arg, for which special rules apply. As a
// special case, top level type parameters pass topLevel=true when checking for
// matches among the constituents of their type set, as type arguments will
// replace the type parameter at compile time.
func (m *argMatcher) match(typ types.Type, topLevel bool) bool {
// %w accepts only errors.
if m.t == argError {
return types.ConvertibleTo(typ, errorType)
}
// If the type implements fmt.Formatter, we have nothing to check.
if isFormatter(typ) {
return true
}
// If we can use a string, might arg (dynamically) implement the Stringer or Error interface?
if m.t&argString != 0 && isConvertibleToString(typ) {
return true
}
if typ, _ := typ.(*typeparams.TypeParam); typ != nil {
// Avoid infinite recursion through type parameters.
if m.seen[typ] {
return true
}
m.seen[typ] = true
terms, err := typeparams.StructuralTerms(typ)
if err != nil {
return true // invalid type (possibly an empty type set)
}
if len(terms) == 0 {
// No restrictions on the underlying of typ. Type parameters implementing
// error, fmt.Formatter, or fmt.Stringer were handled above, and %v and
// %T was handled in matchType. We're about to check restrictions the
// underlying; if the underlying type is unrestricted there must be an
// element of the type set that violates one of the arg type checks
// below, so we can safely return false here.
if m.t == anyType { // anyType must have already been handled.
panic("unexpected printfArgType")
}
return false
}
// Only report a reason if typ is the argument type, otherwise it won't
// make sense. Note that it is not sufficient to check if topLevel == here,
// as type parameters can have a type set consisting of other type
// parameters.
reportReason := len(m.seen) == 1
for _, term := range terms {
if !m.match(term.Type(), topLevel) {
if reportReason {
if term.Tilde() {
m.reason = fmt.Sprintf("contains ~%s", term.Type())
} else {
m.reason = fmt.Sprintf("contains %s", term.Type())
}
}
return false
}
}
return true
}
typ = typ.Underlying()
if m.seen[typ] {
// We've already considered typ, or are in the process of considering it.
// In case we've already considered typ, it must have been valid (else we
// would have stopped matching). In case we're in the process of
// considering it, we must avoid infinite recursion.
//
// There are some pathological cases where returning true here is
// incorrect, for example `type R struct { F []R }`, but these are
// acceptable false negatives.
return true
}
m.seen[typ] = true
switch typ := typ.(type) {
case *types.Signature:
return m.t == argPointer
case *types.Map:
if m.t == argPointer {
return true
}
// Recur: map[int]int matches %d.
return m.match(typ.Key(), false) && m.match(typ.Elem(), false)
case *types.Chan:
return m.t&argPointer != 0
case *types.Array:
// Same as slice.
if types.Identical(typ.Elem().Underlying(), types.Typ[types.Byte]) && m.t&argString != 0 {
return true // %s matches []byte
}
// Recur: []int matches %d.
return m.match(typ.Elem(), false)
case *types.Slice:
// Same as array.
if types.Identical(typ.Elem().Underlying(), types.Typ[types.Byte]) && m.t&argString != 0 {
return true // %s matches []byte
}
if m.t == argPointer {
return true // %p prints a slice's 0th element
}
// Recur: []int matches %d. But watch out for
// type T []T
// If the element is a pointer type (type T[]*T), it's handled fine by the Pointer case below.
return m.match(typ.Elem(), false)
case *types.Pointer:
// Ugly, but dealing with an edge case: a known pointer to an invalid type,
// probably something from a failed import.
if typ.Elem() == types.Typ[types.Invalid] {
return true // special case
}
// If it's actually a pointer with %p, it prints as one.
if m.t == argPointer {
return true
}
if typeparams.IsTypeParam(typ.Elem()) {
return true // We don't know whether the logic below applies. Give up.
}
under := typ.Elem().Underlying()
switch under.(type) {
case *types.Struct: // see below
case *types.Array: // see below
case *types.Slice: // see below
case *types.Map: // see below
default:
// Check whether the rest can print pointers.
return m.t&argPointer != 0
}
// If it's a top-level pointer to a struct, array, slice, type param, or
// map, that's equivalent in our analysis to whether we can
// print the type being pointed to. Pointers in nested levels
// are not supported to minimize fmt running into loops.
if !topLevel {
return false
}
return m.match(under, false)
case *types.Struct:
// report whether all the elements of the struct match the expected type. For
// instance, with "%d" all the elements must be printable with the "%d" format.
for i := 0; i < typ.NumFields(); i++ {
typf := typ.Field(i)
if !m.match(typf.Type(), false) {
return false
}
if m.t&argString != 0 && !typf.Exported() && isConvertibleToString(typf.Type()) {
// Issue #17798: unexported Stringer or error cannot be properly formatted.
return false
}
}
return true
case *types.Interface:
// There's little we can do.
// Whether any particular verb is valid depends on the argument.
// The user may have reasonable prior knowledge of the contents of the interface.
return true
case *types.Basic:
switch typ.Kind() {
case types.UntypedBool,
types.Bool:
return m.t&argBool != 0
case types.UntypedInt,
types.Int,
types.Int8,
types.Int16,
types.Int32,
types.Int64,
types.Uint,
types.Uint8,
types.Uint16,
types.Uint32,
types.Uint64,
types.Uintptr:
return m.t&argInt != 0
case types.UntypedFloat,
types.Float32,
types.Float64:
return m.t&argFloat != 0
case types.UntypedComplex,
types.Complex64,
types.Complex128:
return m.t&argComplex != 0
case types.UntypedString,
types.String:
return m.t&argString != 0
case types.UnsafePointer:
return m.t&(argPointer|argInt) != 0
case types.UntypedRune:
return m.t&(argInt|argRune) != 0
case types.UntypedNil:
return false
case types.Invalid:
return true // Probably a type check problem.
}
panic("unreachable")
}
return false
}
func isConvertibleToString(typ types.Type) bool {
if bt, ok := typ.(*types.Basic); ok && bt.Kind() == types.UntypedNil {
// We explicitly don't want untyped nil, which is
// convertible to both of the interfaces below, as it
// would just panic anyway.
return false
}
if types.ConvertibleTo(typ, errorType) {
return true // via .Error()
}
// Does it implement fmt.Stringer?
if obj, _, _ := types.LookupFieldOrMethod(typ, false, nil, "String"); obj != nil {
if fn, ok := obj.(*types.Func); ok {
sig := fn.Type().(*types.Signature)
if sig.Params().Len() == 0 &&
sig.Results().Len() == 1 &&
sig.Results().At(0).Type() == types.Typ[types.String] {
return true
}
}
}
return false
}