Add staticcheck tool
This commit is contained in:
409
vendor/honnef.co/go/tools/staticcheck/sa5009/sa5009.go
vendored
Normal file
409
vendor/honnef.co/go/tools/staticcheck/sa5009/sa5009.go
vendored
Normal file
@@ -0,0 +1,409 @@
|
||||
package sa5009
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/constant"
|
||||
"go/types"
|
||||
|
||||
"honnef.co/go/tools/analysis/callcheck"
|
||||
"honnef.co/go/tools/analysis/lint"
|
||||
"honnef.co/go/tools/go/ir"
|
||||
"honnef.co/go/tools/go/ir/irutil"
|
||||
"honnef.co/go/tools/go/types/typeutil"
|
||||
"honnef.co/go/tools/internal/passes/buildir"
|
||||
"honnef.co/go/tools/knowledge"
|
||||
"honnef.co/go/tools/printf"
|
||||
|
||||
"golang.org/x/tools/go/analysis"
|
||||
)
|
||||
|
||||
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
|
||||
Analyzer: &analysis.Analyzer{
|
||||
Name: "SA5009",
|
||||
Requires: []*analysis.Analyzer{buildir.Analyzer},
|
||||
Run: callcheck.Analyzer(rules),
|
||||
},
|
||||
Doc: &lint.RawDocumentation{
|
||||
Title: `Invalid Printf call`,
|
||||
Since: "2019.2",
|
||||
Severity: lint.SeverityError,
|
||||
MergeIf: lint.MergeIfAny,
|
||||
},
|
||||
})
|
||||
|
||||
var Analyzer = SCAnalyzer.Analyzer
|
||||
|
||||
// TODO(dh): detect printf wrappers
|
||||
var rules = map[string]callcheck.Check{
|
||||
"fmt.Errorf": func(call *callcheck.Call) { check(call, 0, 1) },
|
||||
"fmt.Printf": func(call *callcheck.Call) { check(call, 0, 1) },
|
||||
"fmt.Sprintf": func(call *callcheck.Call) { check(call, 0, 1) },
|
||||
"fmt.Fprintf": func(call *callcheck.Call) { check(call, 1, 2) },
|
||||
"golang.org/x/xerrors.Errorf": func(call *callcheck.Call) { check(call, 0, 1) },
|
||||
}
|
||||
|
||||
type verbFlag int
|
||||
|
||||
const (
|
||||
isInt verbFlag = 1 << iota
|
||||
isBool
|
||||
isFP
|
||||
isString
|
||||
isPointer
|
||||
// Verbs that accept "pseudo pointers" will sometimes dereference
|
||||
// non-nil pointers. For example, %x on a non-nil *struct will print the
|
||||
// individual fields, but on a nil pointer it will print the address.
|
||||
isPseudoPointer
|
||||
isSlice
|
||||
isAny
|
||||
noRecurse
|
||||
)
|
||||
|
||||
var verbs = [...]verbFlag{
|
||||
'b': isPseudoPointer | isInt | isFP,
|
||||
'c': isInt,
|
||||
'd': isPseudoPointer | isInt,
|
||||
'e': isFP,
|
||||
'E': isFP,
|
||||
'f': isFP,
|
||||
'F': isFP,
|
||||
'g': isFP,
|
||||
'G': isFP,
|
||||
'o': isPseudoPointer | isInt,
|
||||
'O': isPseudoPointer | isInt,
|
||||
'p': isSlice | isPointer | noRecurse,
|
||||
'q': isInt | isString,
|
||||
's': isString,
|
||||
't': isBool,
|
||||
'T': isAny,
|
||||
'U': isInt,
|
||||
'v': isAny,
|
||||
'X': isPseudoPointer | isInt | isFP | isString,
|
||||
'x': isPseudoPointer | isInt | isFP | isString,
|
||||
}
|
||||
|
||||
func check(call *callcheck.Call, fIdx, vIdx int) {
|
||||
f := call.Args[fIdx]
|
||||
var args []ir.Value
|
||||
switch v := call.Args[vIdx].Value.Value.(type) {
|
||||
case *ir.Slice:
|
||||
var ok bool
|
||||
args, ok = irutil.Vararg(v)
|
||||
if !ok {
|
||||
// We don't know what the actual arguments to the function are
|
||||
return
|
||||
}
|
||||
case *ir.Const:
|
||||
// nil, i.e. no arguments
|
||||
default:
|
||||
// We don't know what the actual arguments to the function are
|
||||
return
|
||||
}
|
||||
checkImpl(f, f.Value.Value, args)
|
||||
}
|
||||
|
||||
func checkImpl(carg *callcheck.Argument, f ir.Value, args []ir.Value) {
|
||||
var msCache *typeutil.MethodSetCache
|
||||
if f.Parent() != nil {
|
||||
msCache = &f.Parent().Prog.MethodSets
|
||||
}
|
||||
|
||||
elem := func(T types.Type, verb rune) ([]types.Type, bool) {
|
||||
if verbs[verb]&noRecurse != 0 {
|
||||
return []types.Type{T}, false
|
||||
}
|
||||
switch T := T.(type) {
|
||||
case *types.Slice:
|
||||
if verbs[verb]&isSlice != 0 {
|
||||
return []types.Type{T}, false
|
||||
}
|
||||
if verbs[verb]&isString != 0 && types.Identical(T.Elem().Underlying(), types.Typ[types.Byte]) {
|
||||
return []types.Type{T}, false
|
||||
}
|
||||
return []types.Type{T.Elem()}, true
|
||||
case *types.Map:
|
||||
key := T.Key()
|
||||
val := T.Elem()
|
||||
return []types.Type{key, val}, true
|
||||
case *types.Struct:
|
||||
out := make([]types.Type, 0, T.NumFields())
|
||||
for field := range T.Fields() {
|
||||
out = append(out, field.Type())
|
||||
}
|
||||
return out, true
|
||||
case *types.Array:
|
||||
return []types.Type{T.Elem()}, true
|
||||
default:
|
||||
return []types.Type{T}, false
|
||||
}
|
||||
}
|
||||
isInfo := func(T types.Type, info types.BasicInfo) bool {
|
||||
basic, ok := T.Underlying().(*types.Basic)
|
||||
return ok && basic.Info()&info != 0
|
||||
}
|
||||
|
||||
isFormatter := func(T types.Type, ms *types.MethodSet) bool {
|
||||
sel := ms.Lookup(nil, "Format")
|
||||
if sel == nil {
|
||||
return false
|
||||
}
|
||||
fn, ok := sel.Obj().(*types.Func)
|
||||
if !ok {
|
||||
// should be unreachable
|
||||
return false
|
||||
}
|
||||
sig := fn.Type().(*types.Signature)
|
||||
if sig.Params().Len() != 2 {
|
||||
return false
|
||||
}
|
||||
// TODO(dh): check the types of the arguments for more
|
||||
// precision
|
||||
if sig.Results().Len() != 0 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
var seen typeutil.Map[struct{}]
|
||||
var checkType func(verb rune, T types.Type, top bool) bool
|
||||
checkType = func(verb rune, T types.Type, top bool) bool {
|
||||
if top {
|
||||
seen = typeutil.Map[struct{}]{}
|
||||
}
|
||||
if _, ok := seen.At(T); ok {
|
||||
return true
|
||||
}
|
||||
seen.Set(T, struct{}{})
|
||||
if int(verb) >= len(verbs) {
|
||||
// Unknown verb
|
||||
return true
|
||||
}
|
||||
|
||||
flags := verbs[verb]
|
||||
if flags == 0 {
|
||||
// Unknown verb
|
||||
return true
|
||||
}
|
||||
|
||||
ms := msCache.MethodSet(T)
|
||||
if isFormatter(T, ms) {
|
||||
// the value is responsible for formatting itself
|
||||
return true
|
||||
}
|
||||
|
||||
if flags&isString != 0 && (types.Implements(T, knowledge.Interfaces["fmt.Stringer"]) || types.Implements(T, knowledge.Interfaces["error"])) {
|
||||
// Check for stringer early because we're about to dereference
|
||||
return true
|
||||
}
|
||||
|
||||
T = T.Underlying()
|
||||
if flags&(isPointer|isPseudoPointer) == 0 && top {
|
||||
T = typeutil.Dereference(T)
|
||||
}
|
||||
if flags&isPseudoPointer != 0 && top {
|
||||
t := typeutil.Dereference(T)
|
||||
if _, ok := t.Underlying().(*types.Struct); ok {
|
||||
T = t
|
||||
}
|
||||
}
|
||||
|
||||
if _, ok := T.(*types.Interface); ok {
|
||||
// We don't know what's in the interface
|
||||
return true
|
||||
}
|
||||
|
||||
var info types.BasicInfo
|
||||
if flags&isInt != 0 {
|
||||
info |= types.IsInteger
|
||||
}
|
||||
if flags&isBool != 0 {
|
||||
info |= types.IsBoolean
|
||||
}
|
||||
if flags&isFP != 0 {
|
||||
info |= types.IsFloat | types.IsComplex
|
||||
}
|
||||
if flags&isString != 0 {
|
||||
info |= types.IsString
|
||||
}
|
||||
|
||||
if info != 0 && isInfo(T, info) {
|
||||
return true
|
||||
}
|
||||
|
||||
if flags&isString != 0 {
|
||||
isStringyElem := func(typ types.Type) bool {
|
||||
if typ, ok := typ.Underlying().(*types.Basic); ok {
|
||||
return typ.Kind() == types.Byte
|
||||
}
|
||||
return false
|
||||
}
|
||||
switch T := T.(type) {
|
||||
case *types.Slice:
|
||||
if isStringyElem(T.Elem()) {
|
||||
return true
|
||||
}
|
||||
case *types.Array:
|
||||
if isStringyElem(T.Elem()) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if types.Implements(T, knowledge.Interfaces["fmt.Stringer"]) || types.Implements(T, knowledge.Interfaces["error"]) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if flags&isPointer != 0 && typeutil.IsPointerLike(T) {
|
||||
return true
|
||||
}
|
||||
if flags&isPseudoPointer != 0 {
|
||||
switch U := T.Underlying().(type) {
|
||||
case *types.Pointer:
|
||||
if !top {
|
||||
return true
|
||||
}
|
||||
|
||||
if _, ok := U.Elem().Underlying().(*types.Struct); !ok {
|
||||
// TODO(dh): can this condition ever be false? For
|
||||
// *T, if T is a struct, we'll already have
|
||||
// dereferenced it, meaning the *types.Pointer
|
||||
// branch couldn't have been taken. For T that
|
||||
// aren't structs, this condition will always
|
||||
// evaluate to true.
|
||||
return true
|
||||
}
|
||||
case *types.Chan, *types.Signature:
|
||||
// Channels and functions are always treated as
|
||||
// pointers and never recursed into.
|
||||
return true
|
||||
case *types.Basic:
|
||||
if U.Kind() == types.UnsafePointer {
|
||||
return true
|
||||
}
|
||||
case *types.Interface:
|
||||
// we will already have bailed if the type is an
|
||||
// interface.
|
||||
panic("unreachable")
|
||||
default:
|
||||
// other pointer-like types, such as maps or slices,
|
||||
// will be printed element-wise.
|
||||
}
|
||||
}
|
||||
|
||||
if flags&isSlice != 0 {
|
||||
if _, ok := T.(*types.Slice); ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if flags&isAny != 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
elems, ok := elem(T.Underlying(), verb)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
for _, elem := range elems {
|
||||
if !checkType(verb, elem, false) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
k, ok := irutil.Flatten(f).(*ir.Const)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
actions, err := printf.Parse(constant.StringVal(k.Value))
|
||||
if err != nil {
|
||||
carg.Invalid("couldn't parse format string")
|
||||
return
|
||||
}
|
||||
|
||||
ptr := 1
|
||||
hasExplicit := false
|
||||
|
||||
checkStar := func(verb printf.Verb, star printf.Argument) bool {
|
||||
if star, ok := star.(printf.Star); ok {
|
||||
idx := 0
|
||||
if star.Index == -1 {
|
||||
idx = ptr
|
||||
ptr++
|
||||
} else {
|
||||
hasExplicit = true
|
||||
idx = star.Index
|
||||
ptr = star.Index + 1
|
||||
}
|
||||
if idx == 0 {
|
||||
carg.Invalid(fmt.Sprintf("Printf format %s reads invalid arg 0; indices are 1-based", verb.Raw))
|
||||
return false
|
||||
}
|
||||
if idx > len(args) {
|
||||
carg.Invalid(
|
||||
fmt.Sprintf("Printf format %s reads arg #%d, but call has only %d args",
|
||||
verb.Raw, idx, len(args)))
|
||||
return false
|
||||
}
|
||||
if arg, ok := args[idx-1].(*ir.MakeInterface); ok {
|
||||
if !isInfo(arg.X.Type(), types.IsInteger) {
|
||||
carg.Invalid(fmt.Sprintf("Printf format %s reads non-int arg #%d as argument of *", verb.Raw, idx))
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// We only report one problem per format string. Making a
|
||||
// mistake with an index tends to invalidate all future
|
||||
// implicit indices.
|
||||
for _, action := range actions {
|
||||
verb, ok := action.(printf.Verb)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if !checkStar(verb, verb.Width) || !checkStar(verb, verb.Precision) {
|
||||
return
|
||||
}
|
||||
|
||||
off := ptr
|
||||
if verb.Value != -1 {
|
||||
hasExplicit = true
|
||||
off = verb.Value
|
||||
}
|
||||
if off > len(args) {
|
||||
carg.Invalid(
|
||||
fmt.Sprintf("Printf format %s reads arg #%d, but call has only %d args",
|
||||
verb.Raw, off, len(args)))
|
||||
return
|
||||
} else if verb.Value == 0 && verb.Letter != '%' {
|
||||
carg.Invalid(fmt.Sprintf("Printf format %s reads invalid arg 0; indices are 1-based", verb.Raw))
|
||||
return
|
||||
} else if off != 0 {
|
||||
arg, ok := args[off-1].(*ir.MakeInterface)
|
||||
if ok {
|
||||
if !checkType(verb.Letter, arg.X.Type(), true) {
|
||||
carg.Invalid(fmt.Sprintf("Printf format %s has arg #%d of wrong type %s",
|
||||
verb.Raw, ptr, args[ptr-1].(*ir.MakeInterface).X.Type()))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch verb.Value {
|
||||
case -1:
|
||||
// Consume next argument
|
||||
ptr++
|
||||
case 0:
|
||||
// Don't consume any arguments
|
||||
default:
|
||||
ptr = verb.Value + 1
|
||||
}
|
||||
}
|
||||
|
||||
if !hasExplicit && ptr <= len(args) {
|
||||
carg.Invalid(fmt.Sprintf("Printf call needs %d args but has %d args", ptr-1, len(args)))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user