aboutsummaryrefslogtreecommitdiff
path: root/go/analysis/passes/printf/types.go
blob: 7cbb0bdbf5fbd7aa0f25b76818a7e88042f55073 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
// 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
}