Add staticcheck tool

This commit is contained in:
dwrz
2026-06-06 01:17:20 +00:00
parent 6fb63c8c90
commit ad58cd78ff
315 changed files with 56337 additions and 4 deletions

View 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)))
}
}