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,161 @@
// Package callcheck provides a framework for validating arguments in function calls.
package callcheck
import (
"fmt"
"go/ast"
"go/constant"
"go/types"
"golang.org/x/tools/go/analysis"
"honnef.co/go/tools/analysis/report"
"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"
)
type Call struct {
Pass *analysis.Pass
Instr ir.CallInstruction
Args []*Argument
Parent *ir.Function
invalids []string
}
func (c *Call) Invalid(msg string) {
c.invalids = append(c.invalids, msg)
}
type Argument struct {
Value Value
invalids []string
}
type Value struct {
Value ir.Value
}
func (arg *Argument) Invalid(msg string) {
arg.invalids = append(arg.invalids, msg)
}
type Check func(call *Call)
func Analyzer(rules map[string]Check) func(pass *analysis.Pass) (any, error) {
return func(pass *analysis.Pass) (any, error) {
return checkCalls(pass, rules)
}
}
func checkCalls(pass *analysis.Pass, rules map[string]Check) (any, error) {
cb := func(caller *ir.Function, site ir.CallInstruction, callee *ir.Function) {
obj, ok := callee.Object().(*types.Func)
if !ok {
return
}
r, ok := rules[typeutil.FuncName(obj)]
if !ok {
return
}
var args []*Argument
irargs := site.Common().Args
if callee.Signature.Recv() != nil {
irargs = irargs[1:]
}
for _, arg := range irargs {
if iarg, ok := arg.(*ir.MakeInterface); ok {
arg = iarg.X
}
args = append(args, &Argument{Value: Value{arg}})
}
call := &Call{
Pass: pass,
Instr: site,
Args: args,
Parent: site.Parent(),
}
r(call)
var astcall *ast.CallExpr
switch source := site.Source().(type) {
case *ast.CallExpr:
astcall = source
case *ast.DeferStmt:
astcall = source.Call
case *ast.GoStmt:
astcall = source.Call
case nil:
// TODO(dh): I am not sure this can actually happen. If it
// can't, we should remove this case, and also stop
// checking for astcall == nil in the code that follows.
default:
panic(fmt.Sprintf("unhandled case %T", source))
}
for idx, arg := range call.Args {
for _, e := range arg.invalids {
if astcall != nil {
if idx < len(astcall.Args) {
report.Report(pass, astcall.Args[idx], e)
} else {
// this is an instance of fn1(fn2()) where fn2
// returns multiple values. Report the error
// at the next-best position that we have, the
// first argument. An example of a check that
// triggers this is checkEncodingBinaryRules.
report.Report(pass, astcall.Args[0], e)
}
} else {
report.Report(pass, site, e)
}
}
}
for _, e := range call.invalids {
report.Report(pass, call.Instr, e)
}
}
for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs {
eachCall(fn, cb)
}
return nil, nil
}
func eachCall(fn *ir.Function, cb func(caller *ir.Function, site ir.CallInstruction, callee *ir.Function)) {
for _, b := range fn.Blocks {
for _, instr := range b.Instrs {
if site, ok := instr.(ir.CallInstruction); ok {
if g := site.Common().StaticCallee(); g != nil {
cb(fn, site, g)
}
}
}
}
}
func ExtractConstExpectKind(v Value, kind constant.Kind) *ir.Const {
k := extractConst(v.Value)
if k == nil || k.Value == nil || k.Value.Kind() != kind {
return nil
}
return k
}
func ExtractConst(v Value) *ir.Const {
return extractConst(v.Value)
}
func extractConst(v ir.Value) *ir.Const {
v = irutil.Flatten(v)
switch v := v.(type) {
case *ir.Const:
return v
case *ir.MakeInterface:
return extractConst(v.X)
default:
return nil
}
}

View File

@@ -0,0 +1,589 @@
// Package code answers structural and type questions about Go code.
package code
import (
"fmt"
"go/ast"
"go/build/constraint"
"go/constant"
"go/token"
"go/types"
"go/version"
"path/filepath"
"slices"
"strings"
"honnef.co/go/tools/analysis/facts/generated"
"honnef.co/go/tools/analysis/facts/purity"
"honnef.co/go/tools/analysis/facts/tokenfile"
"honnef.co/go/tools/go/ast/astutil"
"honnef.co/go/tools/go/types/typeutil"
"honnef.co/go/tools/knowledge"
"honnef.co/go/tools/pattern"
"golang.org/x/tools/go/analysis"
)
type Positioner interface {
Pos() token.Pos
}
func IsOfStringConvertibleByteSlice(pass *analysis.Pass, expr ast.Expr) bool {
typ, ok := pass.TypesInfo.TypeOf(expr).Underlying().(*types.Slice)
if !ok {
return false
}
elem := types.Unalias(typ.Elem())
if version.Compare(LanguageVersion(pass, expr), "go1.18") >= 0 {
// Before Go 1.18, one could not directly convert from []T (where 'type T byte')
// to string. See also https://github.com/golang/go/issues/23536.
elem = elem.Underlying()
}
return types.Identical(elem, types.Typ[types.Byte])
}
func IsOfPointerToTypeWithName(pass *analysis.Pass, expr ast.Expr, name string) bool {
ptr, ok := types.Unalias(pass.TypesInfo.TypeOf(expr)).(*types.Pointer)
if !ok {
return false
}
return typeutil.IsTypeWithName(ptr.Elem(), name)
}
func IsOfTypeWithName(pass *analysis.Pass, expr ast.Expr, name string) bool {
return typeutil.IsTypeWithName(pass.TypesInfo.TypeOf(expr), name)
}
func IsInTest(pass *analysis.Pass, node Positioner) bool {
// FIXME(dh): this doesn't work for global variables with
// initializers
f := pass.Fset.File(node.Pos())
return f != nil && strings.HasSuffix(f.Name(), "_test.go")
}
// IsMain reports whether the package being processed is a package
// main.
func IsMain(pass *analysis.Pass) bool {
return pass.Pkg.Name() == "main"
}
// IsMainLike reports whether the package being processed is a
// main-like package. A main-like package is a package that is
// package main, or that is intended to be used by a tool framework
// such as cobra to implement a command.
//
// Note that this function errs on the side of false positives; it may
// return true for packages that aren't main-like. IsMainLike is
// intended for analyses that wish to suppress diagnostics for
// main-like packages to avoid false positives.
func IsMainLike(pass *analysis.Pass) bool {
if pass.Pkg.Name() == "main" {
return true
}
for _, imp := range pass.Pkg.Imports() {
if imp.Path() == "github.com/spf13/cobra" {
return true
}
}
return false
}
func SelectorName(pass *analysis.Pass, expr *ast.SelectorExpr) string {
info := pass.TypesInfo
sel := info.Selections[expr]
if sel == nil {
if x, ok := expr.X.(*ast.Ident); ok {
pkg, ok := info.ObjectOf(x).(*types.PkgName)
if !ok {
// This shouldn't happen
return fmt.Sprintf("%s.%s", x.Name, expr.Sel.Name)
}
return fmt.Sprintf("%s.%s", pkg.Imported().Path(), expr.Sel.Name)
}
panic(fmt.Sprintf("unsupported selector: %v", expr))
}
if v, ok := sel.Obj().(*types.Var); ok && v.IsField() {
return fmt.Sprintf("(%s).%s", typeutil.DereferenceR(sel.Recv()), sel.Obj().Name())
} else {
return fmt.Sprintf("(%s).%s", sel.Recv(), sel.Obj().Name())
}
}
func IsNil(pass *analysis.Pass, expr ast.Expr) bool {
return pass.TypesInfo.Types[expr].IsNil()
}
func BoolConst(pass *analysis.Pass, expr ast.Expr) bool {
val := pass.TypesInfo.ObjectOf(expr.(*ast.Ident)).(*types.Const).Val()
return constant.BoolVal(val)
}
func IsBoolConst(pass *analysis.Pass, expr ast.Expr) bool {
// We explicitly don't support typed bools because more often than
// not, custom bool types are used as binary enums and the explicit
// comparison is desired. We err on the side of false negatives and
// treat aliases like other custom types.
ident, ok := expr.(*ast.Ident)
if !ok {
return false
}
obj := pass.TypesInfo.ObjectOf(ident)
c, ok := obj.(*types.Const)
if !ok {
return false
}
basic, ok := c.Type().(*types.Basic)
if !ok {
return false
}
if basic.Kind() != types.UntypedBool && basic.Kind() != types.Bool {
return false
}
return true
}
func ExprToInt(pass *analysis.Pass, expr ast.Expr) (int64, bool) {
tv := pass.TypesInfo.Types[expr]
if tv.Value == nil {
return 0, false
}
if tv.Value.Kind() != constant.Int {
return 0, false
}
return constant.Int64Val(tv.Value)
}
func ExprToString(pass *analysis.Pass, expr ast.Expr) (string, bool) {
val := pass.TypesInfo.Types[expr].Value
if val == nil {
return "", false
}
if val.Kind() != constant.String {
return "", false
}
return constant.StringVal(val), true
}
func CallName(pass *analysis.Pass, call *ast.CallExpr) string {
// See the comment in typeutil.FuncName for why this doesn't require special handling
// of aliases.
fun := astutil.Unparen(call.Fun)
// Instantiating a function cannot return another generic function, so doing this once is enough
switch idx := fun.(type) {
case *ast.IndexExpr:
fun = idx.X
case *ast.IndexListExpr:
fun = idx.X
}
// (foo)[T] is not a valid instantiation, so no need to unparen again.
switch fun := fun.(type) {
case *ast.SelectorExpr:
fn, ok := pass.TypesInfo.ObjectOf(fun.Sel).(*types.Func)
if !ok {
return ""
}
return typeutil.FuncName(fn)
case *ast.Ident:
obj := pass.TypesInfo.ObjectOf(fun)
switch obj := obj.(type) {
case *types.Func:
return typeutil.FuncName(obj)
case *types.Builtin:
return obj.Name()
default:
return ""
}
default:
return ""
}
}
func IsCallTo(pass *analysis.Pass, node ast.Node, name string) bool {
// See the comment in typeutil.FuncName for why this doesn't require special handling
// of aliases.
call, ok := node.(*ast.CallExpr)
if !ok {
return false
}
return CallName(pass, call) == name
}
func IsCallToAny(pass *analysis.Pass, node ast.Node, names ...string) bool {
// See the comment in typeutil.FuncName for why this doesn't require special handling
// of aliases.
call, ok := node.(*ast.CallExpr)
if !ok {
return false
}
q := CallName(pass, call)
return slices.Contains(names, q)
}
func File(pass *analysis.Pass, node Positioner) *ast.File {
m := pass.ResultOf[tokenfile.Analyzer].(map[*token.File]*ast.File)
return m[pass.Fset.File(node.Pos())]
}
// BuildConstraints returns the build constraints for file f. It considers both //go:build lines as well as
// GOOS and GOARCH in file names.
func BuildConstraints(pass *analysis.Pass, f *ast.File) (constraint.Expr, bool) {
var expr constraint.Expr
for _, cmt := range f.Comments {
if len(cmt.List) == 0 {
continue
}
for _, el := range cmt.List {
if el.Pos() > f.Package {
break
}
if line := el.Text; strings.HasPrefix(line, "//go:build") {
var err error
expr, err = constraint.Parse(line)
if err != nil {
expr = nil
}
break
}
}
}
name := pass.Fset.PositionFor(f.Pos(), false).Filename
oexpr := constraintsFromName(name)
if oexpr != nil {
if expr == nil {
expr = oexpr
} else {
expr = &constraint.AndExpr{X: expr, Y: oexpr}
}
}
return expr, expr != nil
}
func constraintsFromName(name string) constraint.Expr {
name = filepath.Base(name)
name = strings.TrimSuffix(name, ".go")
name = strings.TrimSuffix(name, "_test")
var goos, goarch string
switch strings.Count(name, "_") {
case 0:
// No GOOS or GOARCH in the file name.
case 1:
_, c, _ := strings.Cut(name, "_")
if _, ok := knowledge.KnownGOOS[c]; ok {
goos = c
} else if _, ok := knowledge.KnownGOARCH[c]; ok {
goarch = c
}
default:
n := strings.LastIndex(name, "_")
if _, ok := knowledge.KnownGOOS[name[n+1:]]; ok {
// The file name is *_stuff_GOOS.go
goos = name[n+1:]
} else if _, ok := knowledge.KnownGOARCH[name[n+1:]]; ok {
// The file name is *_GOOS_GOARCH.go or *_stuff_GOARCH.go
goarch = name[n+1:]
_, c, _ := strings.Cut(name[:n], "_")
if _, ok := knowledge.KnownGOOS[c]; ok {
// The file name is *_GOOS_GOARCH.go
goos = c
}
} else {
// The file name could also be something like foo_windows_nonsense.go — and because nonsense
// isn't a known GOARCH, "windows" won't be interpreted as a GOOS, either.
}
}
var expr constraint.Expr
if goos != "" {
expr = &constraint.TagExpr{Tag: goos}
}
if goarch != "" {
if expr == nil {
expr = &constraint.TagExpr{Tag: goarch}
} else {
expr = &constraint.AndExpr{X: expr, Y: &constraint.TagExpr{Tag: goarch}}
}
}
return expr
}
// IsGenerated reports whether pos is in a generated file. It ignores
// //line directives.
func IsGenerated(pass *analysis.Pass, pos token.Pos) bool {
_, ok := Generator(pass, pos)
return ok
}
// Generator returns the generator that generated the file containing
// pos. It ignores //line directives.
func Generator(pass *analysis.Pass, pos token.Pos) (generated.Generator, bool) {
file := pass.Fset.PositionFor(pos, false).Filename
m := pass.ResultOf[generated.Analyzer].(map[string]generated.Generator)
g, ok := m[file]
return g, ok
}
// MayHaveSideEffects reports whether expr may have side effects. If
// the purity argument is nil, this function implements a purely
// syntactic check, meaning that any function call may have side
// effects, regardless of the called function's body. Otherwise,
// purity will be consulted to determine the purity of function calls.
func MayHaveSideEffects(pass *analysis.Pass, expr ast.Expr, purity purity.Result) bool {
switch expr := expr.(type) {
case *ast.BadExpr:
return true
case *ast.Ellipsis:
return MayHaveSideEffects(pass, expr.Elt, purity)
case *ast.FuncLit:
// the literal itself cannot have side effects, only calling it
// might, which is handled by CallExpr.
return false
case *ast.ArrayType, *ast.StructType, *ast.FuncType, *ast.InterfaceType, *ast.MapType, *ast.ChanType:
// types cannot have side effects
return false
case *ast.BasicLit:
return false
case *ast.BinaryExpr:
return MayHaveSideEffects(pass, expr.X, purity) || MayHaveSideEffects(pass, expr.Y, purity)
case *ast.CallExpr:
if purity == nil {
return true
}
switch obj := typeutil.Callee(pass.TypesInfo, expr).(type) {
case *types.Func:
if _, ok := purity[obj]; !ok {
return true
}
case *types.Builtin:
switch obj.Name() {
case "len", "cap":
default:
return true
}
default:
return true
}
for _, arg := range expr.Args {
if MayHaveSideEffects(pass, arg, purity) {
return true
}
}
return false
case *ast.CompositeLit:
if MayHaveSideEffects(pass, expr.Type, purity) {
return true
}
for _, elt := range expr.Elts {
if MayHaveSideEffects(pass, elt, purity) {
return true
}
}
return false
case *ast.Ident:
return false
case *ast.IndexExpr:
return MayHaveSideEffects(pass, expr.X, purity) || MayHaveSideEffects(pass, expr.Index, purity)
case *ast.IndexListExpr:
// In theory, none of the checks are necessary, as IndexListExpr only involves types. But there is no harm in
// being safe.
if MayHaveSideEffects(pass, expr.X, purity) {
return true
}
for _, idx := range expr.Indices {
if MayHaveSideEffects(pass, idx, purity) {
return true
}
}
return false
case *ast.KeyValueExpr:
return MayHaveSideEffects(pass, expr.Key, purity) || MayHaveSideEffects(pass, expr.Value, purity)
case *ast.SelectorExpr:
return MayHaveSideEffects(pass, expr.X, purity)
case *ast.SliceExpr:
return MayHaveSideEffects(pass, expr.X, purity) ||
MayHaveSideEffects(pass, expr.Low, purity) ||
MayHaveSideEffects(pass, expr.High, purity) ||
MayHaveSideEffects(pass, expr.Max, purity)
case *ast.StarExpr:
return MayHaveSideEffects(pass, expr.X, purity)
case *ast.TypeAssertExpr:
return MayHaveSideEffects(pass, expr.X, purity)
case *ast.UnaryExpr:
if MayHaveSideEffects(pass, expr.X, purity) {
return true
}
return expr.Op == token.ARROW || expr.Op == token.AND
case *ast.ParenExpr:
return MayHaveSideEffects(pass, expr.X, purity)
case nil:
return false
default:
panic(fmt.Sprintf("internal error: unhandled type %T", expr))
}
}
// LanguageVersion returns the version of the Go language that node has access to. This
// might differ from the version of the Go standard library.
func LanguageVersion(pass *analysis.Pass, node Positioner) string {
// As of Go 1.21, two places can specify the minimum Go version:
// - 'go' directives in go.mod and go.work files
// - individual files by using '//go:build'
//
// Individual files can upgrade to a higher version than the module version. Individual files
// can also downgrade to a lower version, but only if the module version is at least Go 1.21.
//
// The restriction on downgrading doesn't matter to us. All language changes before Go 1.22 will
// not type-check on versions that are too old, and thus never reach our analyzes. In practice,
// such ineffective downgrading will always be useless, as the compiler will not restrict the
// language features used, and doesn't ever rely on minimum versions to restrict the use of the
// standard library. However, for us, both choices (respecting or ignoring ineffective
// downgrading) have equal complexity, but only respecting it has a non-zero chance of reducing
// noisy positives.
//
// The minimum Go versions are exposed via go/ast.File.GoVersion and go/types.Package.GoVersion.
// ast.File's version is populated by the parser, whereas types.Package's version is populated
// from the Go version specified in the types.Config, which is set by our package loader, based
// on the module information provided by go/packages, via 'go list -json'.
//
// As of Go 1.21, standard library packages do not present themselves as modules, and thus do
// not have a version set on their types.Package. In this case, we fall back to the version
// provided by our '-go' flag. In most cases, '-go' defaults to 'module', which falls back to
// the Go version that Staticcheck was built with when no module information exists. In the
// future, the standard library will hopefully be a proper module (see
// https://github.com/golang/go/issues/61174#issuecomment-1622471317). In that case, the version
// of standard library packages will match that of the used Go version. At that point,
// Staticcheck will refuse to work with Go versions that are too new, to avoid misinterpreting
// code due to language changes.
//
// We also lack module information when building in GOPATH mode. In this case, the implied
// language version is at most Go 1.21, as per https://github.com/golang/go/issues/60915. We
// don't handle this yet, and it will not matter until Go 1.22.
//
// It is not clear how per-file downgrading behaves in GOPATH mode. On the one hand, no module
// version at all is provided, which should preclude per-file downgrading. On the other hand,
// https://github.com/golang/go/issues/60915 suggests that the language version is at most 1.21
// in GOPATH mode, which would allow per-file downgrading. Again it doesn't affect us, as all
// relevant language changes before Go 1.22 will lead to type-checking failures and never reach
// us.
//
// Per-file upgrading is permitted in GOPATH mode.
// If the file has its own Go version, we will return that. Otherwise, we default to
// the type checker's GoVersion, which is populated from either the Go module, or from
// our '-go' flag.
return pass.TypesInfo.FileVersions[File(pass, node)]
}
// StdlibVersion returns the version of the Go standard library that node can expect to
// have access to. This might differ from the language version for versions of Go older
// than 1.21.
func StdlibVersion(pass *analysis.Pass, node Positioner) string {
// The Go version as specified in go.mod or via the '-go' flag
n := pass.Pkg.GoVersion()
f := File(pass, node)
if f == nil {
panic(fmt.Sprintf("no file found for node with position %s", pass.Fset.PositionFor(node.Pos(), false)))
}
if nf := f.GoVersion; nf != "" {
if version.Compare(n, "go1.21") == -1 {
// Before Go 1.21, the Go version set in go.mod specified the maximum language
// version available to the module. It wasn't uncommon to set the version to
// Go 1.20 but restrict usage of 1.20 functionality (both language and stdlib)
// to files tagged for 1.20, and supporting a lower version overall. As such,
// a file tagged lower than the module version couldn't expect to have access
// to the standard library of the version set in go.mod.
//
// At the same time, a file tagged higher than the module version, while not
// able to use newer language features, would still have been able to use a
// newer standard library.
//
// While Go 1.21's behavior has been backported to 1.19.11 and 1.20.6, users'
// expectations have not.
return nf
} else {
// Go 1.21 and newer refuse to build modules that depend on versions newer
// than the used version of the Go toolchain. This means that in a 1.22 module
// with a file tagged as 1.17, the file can expect to have access to 1.22's
// standard library (but not to 1.22 language features). A file tagged with a
// version higher than the minimum version has access to the newer standard
// library (and language features.)
//
// Do note that strictly speaking we're conflating the Go version and the
// module version in our check. Nothing is stopping a user from using Go 1.17
// (which didn't implement the new rules for versions in go.mod) to build a Go
// 1.22 module, in which case a file tagged with go1.17 will not have access to the 1.22
// standard library. However, we believe that if a module requires 1.21 or
// newer, then the author clearly expects the new behavior, and doesn't care
// for the old one. Otherwise they would've specified an older version.
//
// In other words, the module version also specifies what it itself actually means, with
// >=1.21 being a minimum version for the toolchain, and <1.21 being a maximum version for
// the language.
if version.Compare(nf, n) == 1 {
return nf
}
}
}
return n
}
var integerLiteralQ = pattern.MustParse(`(IntegerLiteral tv)`)
func IntegerLiteral(pass *analysis.Pass, node ast.Node) (types.TypeAndValue, bool) {
m, ok := Match(pass, integerLiteralQ, node)
if !ok {
return types.TypeAndValue{}, false
}
return m.State["tv"].(types.TypeAndValue), true
}
func IsIntegerLiteral(pass *analysis.Pass, node ast.Node, value constant.Value) bool {
tv, ok := IntegerLiteral(pass, node)
if !ok {
return false
}
return constant.Compare(tv.Value, token.EQL, value)
}
// IsMethod reports whether expr is a method call of a named method with signature meth.
// If name is empty, it is not checked.
// For now, method expressions (Type.Method(recv, ..)) are not considered method calls.
func IsMethod(pass *analysis.Pass, expr *ast.SelectorExpr, name string, meth *types.Signature) bool {
if name != "" && expr.Sel.Name != name {
return false
}
sel, ok := pass.TypesInfo.Selections[expr]
if !ok || sel.Kind() != types.MethodVal {
return false
}
return types.Identical(sel.Type(), meth)
}
func RefersTo(pass *analysis.Pass, expr ast.Expr, ident types.Object) bool {
found := false
fn := func(node ast.Node) bool {
ident2, ok := node.(*ast.Ident)
if !ok {
return true
}
if ident == pass.TypesInfo.ObjectOf(ident2) {
found = true
return false
}
return true
}
ast.Inspect(expr, fn)
return found
}

View File

@@ -0,0 +1,153 @@
package code
import (
"bytes"
"fmt"
"go/ast"
"go/format"
"go/types"
"iter"
"slices"
typeindexanalyzer "honnef.co/go/tools/internal/analysisinternal/typeindex"
"honnef.co/go/tools/internal/typesinternal/typeindex"
"honnef.co/go/tools/pattern"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector"
)
var RequiredAnalyzers = []*analysis.Analyzer{inspect.Analyzer, typeindexanalyzer.Analyzer}
func Cursor(pass *analysis.Pass) inspector.Cursor {
return pass.ResultOf[inspect.Analyzer].(*inspector.Inspector).Root()
}
func Preorder(pass *analysis.Pass, fn func(ast.Node), types ...ast.Node) {
pass.ResultOf[inspect.Analyzer].(*inspector.Inspector).Preorder(types, fn)
}
func PreorderStack(pass *analysis.Pass, fn func(ast.Node, []ast.Node), types ...ast.Node) {
pass.ResultOf[inspect.Analyzer].(*inspector.Inspector).WithStack(types, func(n ast.Node, push bool, stack []ast.Node) (proceed bool) {
if push {
fn(n, stack)
}
return true
})
}
func Matches(pass *analysis.Pass, qs ...pattern.Pattern) iter.Seq2[ast.Node, *pattern.Matcher] {
return func(yield func(ast.Node, *pattern.Matcher) bool) {
for _, q := range qs {
if !CouldMatchAny(pass, q) {
continue
}
if len(q.RootCallSymbols) != 0 {
index := pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
for _, isym := range q.RootCallSymbols {
var obj types.Object
if isym.Type == "" {
obj = index.Object(isym.Path, isym.Ident)
} else {
obj = index.Selection(isym.Path, isym.Type, isym.Ident)
}
for c := range index.Calls(obj) {
node := c.Node()
if m, ok := Match(pass, q, node); ok {
if !yield(node, m) {
return
}
}
}
}
} else {
ins := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
fn := func(node ast.Node, push bool) bool {
if !push {
return true
}
if m, ok := Match(pass, q, node); ok {
return yield(node, m)
}
return true
}
ins.Nodes(q.EntryNodes, fn)
}
}
}
}
func Match(pass *analysis.Pass, q pattern.Pattern, node ast.Node) (*pattern.Matcher, bool) {
// Note that we ignore q.Relevant callers of Match usually use
// AST inspectors that already filter on nodes we're interested
// in.
m := &pattern.Matcher{TypesInfo: pass.TypesInfo}
ok := m.Match(q, node)
return m, ok
}
func CouldMatchAny(pass *analysis.Pass, qs ...pattern.Pattern) bool {
index := pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
var do func(node pattern.Node) bool
do = func(node pattern.Node) bool {
switch node := node.(type) {
case pattern.Any:
return true
case pattern.Or:
return slices.ContainsFunc(node.Nodes, do)
case pattern.And:
for _, child := range node.Nodes {
if !do(child) {
return false
}
}
return true
case pattern.IndexSymbol:
if node.Type == "" {
return index.Object(node.Path, node.Ident) != nil
} else {
return index.Selection(node.Path, node.Type, node.Ident) != nil
}
default:
panic(fmt.Sprintf("internal error: unexpected type %T", node))
}
}
for _, q := range qs {
if do(q.SymbolsPattern) {
return true
}
}
return false
}
func MatchAndEdit(pass *analysis.Pass, before, after pattern.Pattern, node ast.Node) (*pattern.Matcher, []analysis.TextEdit, bool) {
m, ok := Match(pass, before, node)
if !ok {
return m, nil, false
}
r := pattern.NodeToAST(after.Root, m.State)
buf := &bytes.Buffer{}
format.Node(buf, pass.Fset, r)
edit := []analysis.TextEdit{{
Pos: node.Pos(),
End: node.End(),
NewText: buf.Bytes(),
}}
return m, edit, true
}
func EditMatch(pass *analysis.Pass, node ast.Node, m *pattern.Matcher, after pattern.Pattern) []analysis.TextEdit {
r := pattern.NodeToAST(after.Root, m.State)
buf := &bytes.Buffer{}
format.Node(buf, pass.Fset, r)
edit := []analysis.TextEdit{{
Pos: node.Pos(),
End: node.End(),
NewText: buf.Bytes(),
}}
return edit
}

View File

@@ -0,0 +1,83 @@
// Package edit contains helpers for creating suggested fixes.
package edit
import (
"bytes"
"go/ast"
"go/format"
"go/token"
"golang.org/x/tools/go/analysis"
"honnef.co/go/tools/pattern"
)
// Ranger describes values that have a start and end position.
// In most cases these are either ast.Node or manually constructed ranges.
type Ranger interface {
Pos() token.Pos
End() token.Pos
}
// Range implements the Ranger interface.
type Range [2]token.Pos
func (r Range) Pos() token.Pos { return r[0] }
func (r Range) End() token.Pos { return r[1] }
// ReplaceWithString replaces a range with a string.
func ReplaceWithString(old Ranger, new string) analysis.TextEdit {
return analysis.TextEdit{
Pos: old.Pos(),
End: old.End(),
NewText: []byte(new),
}
}
// ReplaceWithNode replaces a range with an AST node.
func ReplaceWithNode(fset *token.FileSet, old Ranger, new ast.Node) analysis.TextEdit {
buf := &bytes.Buffer{}
if err := format.Node(buf, fset, new); err != nil {
panic("internal error: " + err.Error())
}
return analysis.TextEdit{
Pos: old.Pos(),
End: old.End(),
NewText: buf.Bytes(),
}
}
// ReplaceWithPattern replaces a range with the result of executing a pattern.
func ReplaceWithPattern(fset *token.FileSet, old Ranger, new pattern.Pattern, state pattern.State) analysis.TextEdit {
r := pattern.NodeToAST(new.Root, state)
buf := &bytes.Buffer{}
format.Node(buf, fset, r)
return analysis.TextEdit{
Pos: old.Pos(),
End: old.End(),
NewText: buf.Bytes(),
}
}
// Delete deletes a range of code.
func Delete(old Ranger) analysis.TextEdit {
return analysis.TextEdit{
Pos: old.Pos(),
End: old.End(),
NewText: nil,
}
}
func Fix(msg string, edits ...analysis.TextEdit) analysis.SuggestedFix {
return analysis.SuggestedFix{
Message: msg,
TextEdits: edits,
}
}
// Selector creates a new selector expression.
func Selector(x, sel string) *ast.SelectorExpr {
return &ast.SelectorExpr{
X: &ast.Ident{Name: x},
Sel: &ast.Ident{Name: sel},
}
}

View File

@@ -0,0 +1,154 @@
package deprecated
import (
"go/ast"
"go/token"
"go/types"
"reflect"
"strings"
"golang.org/x/tools/go/analysis"
)
type IsDeprecated struct{ Msg string }
func (*IsDeprecated) AFact() {}
func (d *IsDeprecated) String() string { return "Deprecated: " + d.Msg }
type Result struct {
Objects map[types.Object]*IsDeprecated
Packages map[*types.Package]*IsDeprecated
}
var Analyzer = &analysis.Analyzer{
Name: "fact_deprecated",
Doc: "Mark deprecated objects",
Run: deprecated,
FactTypes: []analysis.Fact{(*IsDeprecated)(nil)},
ResultType: reflect.TypeFor[Result](),
}
func deprecated(pass *analysis.Pass) (any, error) {
var names []*ast.Ident
extractDeprecatedMessage := func(docs []*ast.CommentGroup) string {
for _, doc := range docs {
if doc == nil {
continue
}
parts := strings.SplitSeq(doc.Text(), "\n\n")
for part := range parts {
if !strings.HasPrefix(part, "Deprecated: ") {
continue
}
alt := part[len("Deprecated: "):]
alt = strings.Replace(alt, "\n", " ", -1)
return alt
}
}
return ""
}
doDocs := func(names []*ast.Ident, docs []*ast.CommentGroup) {
alt := extractDeprecatedMessage(docs)
if alt == "" {
return
}
for _, name := range names {
obj := pass.TypesInfo.ObjectOf(name)
pass.ExportObjectFact(obj, &IsDeprecated{alt})
}
}
var docs []*ast.CommentGroup
for _, f := range pass.Files {
docs = append(docs, f.Doc)
}
if alt := extractDeprecatedMessage(docs); alt != "" {
// Don't mark package syscall as deprecated, even though
// it is. A lot of people still use it for simple
// constants like SIGKILL, and I am not comfortable
// telling them to use x/sys for that.
if pass.Pkg.Path() != "syscall" {
pass.ExportPackageFact(&IsDeprecated{alt})
}
}
docs = docs[:0]
for _, f := range pass.Files {
fn := func(node ast.Node) bool {
if node == nil {
return true
}
var ret bool
switch node := node.(type) {
case *ast.GenDecl:
switch node.Tok {
case token.TYPE, token.CONST, token.VAR:
docs = append(docs, node.Doc)
for i := range node.Specs {
switch n := node.Specs[i].(type) {
case *ast.ValueSpec:
names = append(names, n.Names...)
case *ast.TypeSpec:
names = append(names, n.Name)
}
}
ret = true
default:
return false
}
case *ast.FuncDecl:
docs = append(docs, node.Doc)
names = []*ast.Ident{node.Name}
ret = false
case *ast.TypeSpec:
docs = append(docs, node.Doc)
names = []*ast.Ident{node.Name}
ret = true
case *ast.ValueSpec:
docs = append(docs, node.Doc)
names = node.Names
ret = false
case *ast.File:
return true
case *ast.StructType:
for _, field := range node.Fields.List {
doDocs(field.Names, []*ast.CommentGroup{field.Doc})
}
return false
case *ast.InterfaceType:
for _, field := range node.Methods.List {
doDocs(field.Names, []*ast.CommentGroup{field.Doc})
}
return false
default:
return false
}
if len(names) == 0 || len(docs) == 0 {
return ret
}
doDocs(names, docs)
docs = docs[:0]
names = nil
return ret
}
ast.Inspect(f, fn)
}
out := Result{
Objects: map[types.Object]*IsDeprecated{},
Packages: map[*types.Package]*IsDeprecated{},
}
for _, fact := range pass.AllObjectFacts() {
out.Objects[fact.Object] = fact.Fact.(*IsDeprecated)
}
for _, fact := range pass.AllPackageFacts() {
out.Packages[fact.Package] = fact.Fact.(*IsDeprecated)
}
return out, nil
}

View File

@@ -0,0 +1,20 @@
package directives
import (
"reflect"
"golang.org/x/tools/go/analysis"
"honnef.co/go/tools/analysis/lint"
)
func directives(pass *analysis.Pass) (any, error) {
return lint.ParseDirectives(pass.Files, pass.Fset), nil
}
var Analyzer = &analysis.Analyzer{
Name: "directives",
Doc: "extracts linter directives",
Run: directives,
RunDespiteErrors: true,
ResultType: reflect.TypeFor[[]lint.Directive](),
}

View File

@@ -0,0 +1,97 @@
package generated
import (
"bufio"
"bytes"
"io"
"os"
"reflect"
"strings"
"golang.org/x/tools/go/analysis"
)
type Generator int
// A list of known generators we can detect
const (
Unknown Generator = iota
Goyacc
Cgo
Stringer
ProtocGenGo
)
var (
// used by cgo before Go 1.11
oldCgo = []byte("// Created by cgo - DO NOT EDIT")
prefix = []byte("// Code generated ")
suffix = []byte(" DO NOT EDIT.")
nl = []byte("\n")
crnl = []byte("\r\n")
)
func isGenerated(path string) (Generator, bool) {
f, err := os.Open(path)
if err != nil {
return 0, false
}
defer f.Close()
br := bufio.NewReader(f)
for {
s, err := br.ReadBytes('\n')
if err != nil && err != io.EOF {
return 0, false
}
s = bytes.TrimSuffix(s, crnl)
s = bytes.TrimSuffix(s, nl)
if bytes.HasPrefix(s, prefix) && bytes.HasSuffix(s, suffix) {
if len(s)-len(suffix) < len(prefix) {
return Unknown, true
}
text := string(s[len(prefix) : len(s)-len(suffix)])
switch text {
case "by goyacc.":
return Goyacc, true
case "by cmd/cgo;":
return Cgo, true
case "by protoc-gen-go.":
return ProtocGenGo, true
}
if strings.HasPrefix(text, `by "stringer `) {
return Stringer, true
}
if strings.HasPrefix(text, `by goyacc `) {
return Goyacc, true
}
return Unknown, true
}
if bytes.Equal(s, oldCgo) {
return Cgo, true
}
if err == io.EOF {
break
}
}
return 0, false
}
var Analyzer = &analysis.Analyzer{
Name: "isgenerated",
Doc: "annotate file names that have been code generated",
Run: func(pass *analysis.Pass) (any, error) {
m := map[string]Generator{}
for _, f := range pass.Files {
path := pass.Fset.PositionFor(f.Pos(), false).Filename
g, ok := isGenerated(path)
if ok {
m[path] = g
}
}
return m, nil
},
RunDespiteErrors: true,
ResultType: reflect.TypeFor[map[string]Generator](),
}

View File

@@ -0,0 +1,255 @@
package nilness
import (
"fmt"
"go/token"
"go/types"
"reflect"
"honnef.co/go/tools/go/ir"
"honnef.co/go/tools/go/types/typeutil"
"honnef.co/go/tools/internal/passes/buildir"
"golang.org/x/tools/go/analysis"
)
// neverReturnsNilFact denotes that a function's return value will never
// be nil (typed or untyped). The analysis errs on the side of false
// negatives.
type neverReturnsNilFact struct {
Rets []neverNilness
}
func (*neverReturnsNilFact) AFact() {}
func (fact *neverReturnsNilFact) String() string {
return fmt.Sprintf("never returns nil: %v", fact.Rets)
}
type Result struct {
m map[*types.Func][]neverNilness
}
var Analysis = &analysis.Analyzer{
Name: "nilness",
Doc: "Annotates return values that will never be nil (typed or untyped)",
Run: run,
Requires: []*analysis.Analyzer{buildir.Analyzer},
FactTypes: []analysis.Fact{(*neverReturnsNilFact)(nil)},
ResultType: reflect.TypeFor[*Result](),
}
// MayReturnNil reports whether the ret's return value of fn might be
// a typed or untyped nil value. The value of ret is zero-based. When
// globalOnly is true, the only possible nil values are global
// variables.
//
// The analysis has false positives: MayReturnNil can incorrectly
// report true, but never incorrectly reports false.
func (r *Result) MayReturnNil(fn *types.Func, ret int) (yes bool, globalOnly bool) {
if !typeutil.IsPointerLike(fn.Type().(*types.Signature).Results().At(ret).Type()) {
return false, false
}
if len(r.m[fn]) == 0 {
return true, false
}
v := r.m[fn][ret]
return v != neverNil, v == onlyGlobal
}
func run(pass *analysis.Pass) (any, error) {
seen := map[*ir.Function]struct{}{}
out := &Result{
m: map[*types.Func][]neverNilness{},
}
for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs {
impl(pass, fn, seen)
}
for _, fact := range pass.AllObjectFacts() {
out.m[fact.Object.(*types.Func)] = fact.Fact.(*neverReturnsNilFact).Rets
}
return out, nil
}
type neverNilness uint8
const (
neverNil neverNilness = 1
onlyGlobal neverNilness = 2
nilly neverNilness = 3
)
func (n neverNilness) String() string {
switch n {
case neverNil:
return "never"
case onlyGlobal:
return "global"
case nilly:
return "nil"
default:
return "BUG"
}
}
func impl(pass *analysis.Pass, fn *ir.Function, seenFns map[*ir.Function]struct{}) []neverNilness {
if fn.Object() == nil {
// TODO(dh): support closures
return nil
}
if fact := new(neverReturnsNilFact); pass.ImportObjectFact(fn.Object(), fact) {
return fact.Rets
}
if fn.Pkg != pass.ResultOf[buildir.Analyzer].(*buildir.IR).Pkg {
return nil
}
if fn.Blocks == nil {
return nil
}
if _, ok := seenFns[fn]; ok {
// break recursion
return nil
}
seenFns[fn] = struct{}{}
seen := map[ir.Value]struct{}{}
var mightReturnNil func(v ir.Value) neverNilness
mightReturnNil = func(v ir.Value) neverNilness {
if _, ok := seen[v]; ok {
// break cycle
return nilly
}
if !typeutil.IsPointerLike(v.Type()) {
return neverNil
}
seen[v] = struct{}{}
switch v := v.(type) {
case *ir.MakeInterface:
return mightReturnNil(v.X)
case *ir.Convert:
return mightReturnNil(v.X)
case *ir.SliceToArrayPointer:
if typeutil.CoreType(v.Type()).(*types.Pointer).Elem().Underlying().(*types.Array).Len() == 0 {
return mightReturnNil(v.X)
} else {
// converting a slice to an array pointer of length > 0 panics if the slice is nil
return neverNil
}
case *ir.Slice:
return mightReturnNil(v.X)
case *ir.Phi:
ret := neverNil
for _, e := range v.Edges {
if n := mightReturnNil(e); n > ret {
ret = n
}
}
return ret
case *ir.Extract:
switch d := v.Tuple.(type) {
case *ir.Call:
if callee := d.Call.StaticCallee(); callee != nil {
ret := impl(pass, callee, seenFns)
if len(ret) == 0 {
return nilly
}
return ret[v.Index]
} else {
return nilly
}
case *ir.TypeAssert, *ir.Next, *ir.Select, *ir.MapLookup, *ir.TypeSwitch, *ir.Recv, *ir.Sigma:
// we don't need to look at the Extract's index
// because we've already checked its type.
return nilly
default:
panic(fmt.Sprintf("internal error: unhandled type %T", d))
}
case *ir.Call:
if callee := v.Call.StaticCallee(); callee != nil {
ret := impl(pass, callee, seenFns)
if len(ret) == 0 {
return nilly
}
return ret[0]
} else {
return nilly
}
case *ir.BinOp, *ir.UnOp, *ir.Alloc, *ir.FieldAddr, *ir.IndexAddr, *ir.Global, *ir.MakeSlice, *ir.MakeClosure, *ir.Function, *ir.MakeMap, *ir.MakeChan:
return neverNil
case *ir.Sigma:
iff, ok := v.From.Control().(*ir.If)
if !ok {
return nilly
}
binop, ok := iff.Cond.(*ir.BinOp)
if !ok {
return nilly
}
isNil := func(v ir.Value) bool {
k, ok := v.(*ir.Const)
if !ok {
return false
}
return k.Value == nil
}
if binop.X == v.X && isNil(binop.Y) || binop.Y == v.X && isNil(binop.X) {
op := binop.Op
if v.From.Succs[0] != v.Block() {
// we're in the false branch, negate op
switch op {
case token.EQL:
op = token.NEQ
case token.NEQ:
op = token.EQL
default:
panic(fmt.Sprintf("internal error: unhandled token %v", op))
}
}
switch op {
case token.EQL:
return nilly
case token.NEQ:
return neverNil
default:
panic(fmt.Sprintf("internal error: unhandled token %v", op))
}
}
return nilly
case *ir.ChangeType:
return mightReturnNil(v.X)
case *ir.MultiConvert:
return mightReturnNil(v.X)
case *ir.Load:
if _, ok := v.X.(*ir.Global); ok {
return onlyGlobal
}
return nilly
case *ir.AggregateConst:
return neverNil
case *ir.TypeAssert, *ir.ChangeInterface, *ir.Field, *ir.Const, *ir.GenericConst, *ir.Index, *ir.MapLookup, *ir.Parameter, *ir.Recv, *ir.TypeSwitch:
return nilly
default:
panic(fmt.Sprintf("internal error: unhandled type %T", v))
}
}
ret := fn.Exit.Control().(*ir.Return)
out := make([]neverNilness, len(ret.Results))
export := false
for i, v := range ret.Results {
// OPT(dh): couldn't we check the result type's pointer-likeness early, and skip
// processing the return value altogether?
v := mightReturnNil(v)
out[i] = v
if v != nilly && typeutil.IsPointerLike(fn.Signature.Results().At(i).Type()) {
export = true
}
}
if export {
pass.ExportObjectFact(fn.Object(), &neverReturnsNilFact{out})
}
return out
}

View File

@@ -0,0 +1,264 @@
package purity
// TODO(dh): we should split this into two facts, one tracking actual purity, and one tracking side-effects. A function
// that returns a heap allocation isn't pure, but it may be free of side effects.
import (
"go/types"
"reflect"
"honnef.co/go/tools/go/ir"
"honnef.co/go/tools/go/ir/irutil"
"honnef.co/go/tools/internal/passes/buildir"
"golang.org/x/tools/go/analysis"
)
type IsPure struct{}
func (*IsPure) AFact() {}
func (d *IsPure) String() string { return "is pure" }
type Result map[*types.Func]*IsPure
var Analyzer = &analysis.Analyzer{
Name: "fact_purity",
Doc: "Mark pure functions",
Run: purity,
Requires: []*analysis.Analyzer{buildir.Analyzer},
FactTypes: []analysis.Fact{(*IsPure)(nil)},
ResultType: reflect.TypeFor[Result](),
}
var pureStdlib = map[string]struct{}{
"errors.New": {},
"fmt.Errorf": {},
"fmt.Sprintf": {},
"fmt.Sprint": {},
"sort.Reverse": {},
"strings.Map": {},
"strings.Repeat": {},
"strings.Replace": {},
"strings.Title": {},
"strings.ToLower": {},
"strings.ToLowerSpecial": {},
"strings.ToTitle": {},
"strings.ToTitleSpecial": {},
"strings.ToUpper": {},
"strings.ToUpperSpecial": {},
"strings.Trim": {},
"strings.TrimFunc": {},
"strings.TrimLeft": {},
"strings.TrimLeftFunc": {},
"strings.TrimPrefix": {},
"strings.TrimRight": {},
"strings.TrimRightFunc": {},
"strings.TrimSpace": {},
"strings.TrimSuffix": {},
"(*net/http.Request).WithContext": {},
"time.Now": {},
"time.Parse": {},
"time.ParseInLocation": {},
"time.Unix": {},
"time.UnixMicro": {},
"time.UnixMilli": {},
"(time.Time).Add": {},
"(time.Time).AddDate": {},
"(time.Time).After": {},
"(time.Time).Before": {},
"(time.Time).Clock": {},
"(time.Time).Compare": {},
"(time.Time).Date": {},
"(time.Time).Day": {},
"(time.Time).Equal": {},
"(time.Time).Format": {},
"(time.Time).GoString": {},
"(time.Time).GobEncode": {},
"(time.Time).Hour": {},
"(time.Time).ISOWeek": {},
"(time.Time).In": {},
"(time.Time).IsDST": {},
"(time.Time).IsZero": {},
"(time.Time).Local": {},
"(time.Time).Location": {},
"(time.Time).MarshalBinary": {},
"(time.Time).MarshalJSON": {},
"(time.Time).MarshalText": {},
"(time.Time).Minute": {},
"(time.Time).Month": {},
"(time.Time).Nanosecond": {},
"(time.Time).Round": {},
"(time.Time).Second": {},
"(time.Time).String": {},
"(time.Time).Sub": {},
"(time.Time).Truncate": {},
"(time.Time).UTC": {},
"(time.Time).Unix": {},
"(time.Time).UnixMicro": {},
"(time.Time).UnixMilli": {},
"(time.Time).UnixNano": {},
"(time.Time).Weekday": {},
"(time.Time).Year": {},
"(time.Time).YearDay": {},
"(time.Time).Zone": {},
"(time.Time).ZoneBounds": {},
}
func purity(pass *analysis.Pass) (any, error) {
seen := map[*ir.Function]struct{}{}
irpkg := pass.ResultOf[buildir.Analyzer].(*buildir.IR).Pkg
var check func(fn *ir.Function) (ret bool)
check = func(fn *ir.Function) (ret bool) {
if fn.Object() == nil {
// TODO(dh): support closures
return false
}
if pass.ImportObjectFact(fn.Object(), new(IsPure)) {
return true
}
if fn.Pkg != irpkg {
// Function is in another package but wasn't marked as
// pure, ergo it isn't pure
return false
}
// Break recursion
if _, ok := seen[fn]; ok {
return false
}
seen[fn] = struct{}{}
defer func() {
if ret {
pass.ExportObjectFact(fn.Object(), &IsPure{})
}
}()
if irutil.IsStub(fn) {
return false
}
if _, ok := pureStdlib[fn.Object().(*types.Func).FullName()]; ok {
return true
}
if fn.Signature.Results().Len() == 0 {
// A function with no return values is empty or is doing some
// work we cannot see (for example because of build tags);
// don't consider it pure.
return false
}
var isBasic func(typ types.Type) bool
isBasic = func(typ types.Type) bool {
switch u := typ.Underlying().(type) {
case *types.Basic:
return true
case *types.Struct:
for field := range u.Fields() {
if !isBasic(field.Type()) {
return false
}
}
return true
default:
return false
}
}
for _, param := range fn.Params {
// TODO(dh): this may not be strictly correct. pure code can, to an extent, operate on non-basic types.
if !isBasic(param.Type()) {
return false
}
}
// Don't consider external functions pure.
if fn.Blocks == nil {
return false
}
checkCall := func(common *ir.CallCommon) bool {
if common.IsInvoke() {
return false
}
builtin, ok := common.Value.(*ir.Builtin)
if !ok {
if common.StaticCallee() != fn {
if common.StaticCallee() == nil {
return false
}
if !check(common.StaticCallee()) {
return false
}
}
} else {
switch builtin.Name() {
case "len", "cap":
default:
return false
}
}
return true
}
var isStackAddr func(ir.Value) bool
isStackAddr = func(v ir.Value) bool {
switch v := v.(type) {
case *ir.Alloc:
return !v.Heap
case *ir.FieldAddr:
return isStackAddr(v.X)
default:
return false
}
}
for _, b := range fn.Blocks {
for _, ins := range b.Instrs {
switch ins := ins.(type) {
case *ir.Call:
if !checkCall(ins.Common()) {
return false
}
case *ir.Defer:
if !checkCall(&ins.Call) {
return false
}
case *ir.Select:
return false
case *ir.Send:
return false
case *ir.Go:
return false
case *ir.Panic:
return false
case *ir.Store:
if !isStackAddr(ins.Addr) {
return false
}
case *ir.FieldAddr:
if !isStackAddr(ins.X) {
return false
}
case *ir.Alloc:
// TODO(dh): make use of proper escape analysis
if ins.Heap {
return false
}
case *ir.Load:
if !isStackAddr(ins.X) {
return false
}
}
}
}
return true
}
for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs {
check(fn)
}
out := Result{}
for _, fact := range pass.AllObjectFacts() {
out[fact.Object.(*types.Func)] = fact.Fact.(*IsPure)
}
return out, nil
}

View File

@@ -0,0 +1,24 @@
package tokenfile
import (
"go/ast"
"go/token"
"reflect"
"golang.org/x/tools/go/analysis"
)
var Analyzer = &analysis.Analyzer{
Name: "tokenfileanalyzer",
Doc: "creates a mapping of *token.File to *ast.File",
Run: func(pass *analysis.Pass) (any, error) {
m := map[*token.File]*ast.File{}
for _, af := range pass.Files {
tf := pass.Fset.File(af.Pos())
m[tf] = af
}
return m, nil
},
RunDespiteErrors: true,
ResultType: reflect.TypeFor[map[*token.File]*ast.File](),
}

View File

@@ -0,0 +1,253 @@
package typedness
import (
"fmt"
"go/token"
"go/types"
"reflect"
"honnef.co/go/tools/go/ir"
"honnef.co/go/tools/go/ir/irutil"
"honnef.co/go/tools/internal/passes/buildir"
"golang.org/x/exp/typeparams"
"golang.org/x/tools/go/analysis"
)
// alwaysTypedFact denotes that a function's return value will never
// be untyped nil. The analysis errs on the side of false negatives.
type alwaysTypedFact struct {
Rets uint8
}
func (*alwaysTypedFact) AFact() {}
func (fact *alwaysTypedFact) String() string {
return fmt.Sprintf("always typed: %08b", fact.Rets)
}
type Result struct {
m map[*types.Func]uint8
}
var Analysis = &analysis.Analyzer{
Name: "typedness",
Doc: "Annotates return values that are always typed values",
Run: run,
Requires: []*analysis.Analyzer{buildir.Analyzer},
FactTypes: []analysis.Fact{(*alwaysTypedFact)(nil)},
ResultType: reflect.TypeFor[*Result](),
}
// MustReturnTyped reports whether the ret's return value of fn must
// be a typed value, i.e. an interface value containing a concrete
// type or trivially a concrete type. The value of ret is zero-based.
//
// The analysis has false negatives: MustReturnTyped may incorrectly
// report false, but never incorrectly reports true.
func (r *Result) MustReturnTyped(fn *types.Func, ret int) bool {
if _, ok := fn.Type().(*types.Signature).Results().At(ret).Type().Underlying().(*types.Interface); !ok {
return true
}
return (r.m[fn] & (1 << ret)) != 0
}
func run(pass *analysis.Pass) (any, error) {
seen := map[*ir.Function]struct{}{}
out := &Result{
m: map[*types.Func]uint8{},
}
for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs {
impl(pass, fn, seen)
}
for _, fact := range pass.AllObjectFacts() {
out.m[fact.Object.(*types.Func)] = fact.Fact.(*alwaysTypedFact).Rets
}
return out, nil
}
func impl(pass *analysis.Pass, fn *ir.Function, seenFns map[*ir.Function]struct{}) (out uint8) {
if fn.Signature.Results().Len() > 8 {
return 0
}
if fn.Object() == nil {
// TODO(dh): support closures
return 0
}
if fact := new(alwaysTypedFact); pass.ImportObjectFact(fn.Object(), fact) {
return fact.Rets
}
if fn.Pkg != pass.ResultOf[buildir.Analyzer].(*buildir.IR).Pkg {
return 0
}
if fn.Blocks == nil {
return 0
}
if irutil.IsStub(fn) {
return 0
}
if _, ok := seenFns[fn]; ok {
// break recursion
return 0
}
seenFns[fn] = struct{}{}
defer func() {
for i := 0; i < fn.Signature.Results().Len(); i++ {
if _, ok := fn.Signature.Results().At(i).Type().Underlying().(*types.Interface); !ok {
// we don't need facts to know that non-interface
// types can't be untyped nil. zeroing out those bits
// may result in all bits being zero, in which case we
// don't have to save any fact.
out &= ^(1 << i)
}
}
if out > 0 {
pass.ExportObjectFact(fn.Object(), &alwaysTypedFact{out})
}
}()
isUntypedNil := func(v ir.Value) bool {
k, ok := v.(*ir.Const)
if !ok {
return false
}
if _, ok := k.Type().Underlying().(*types.Interface); !ok {
return false
}
return k.Value == nil
}
var do func(v ir.Value, seen map[ir.Value]struct{}) bool
do = func(v ir.Value, seen map[ir.Value]struct{}) bool {
if _, ok := seen[v]; ok {
// break cycle
return false
}
seen[v] = struct{}{}
switch v := v.(type) {
case *ir.Const:
// can't be a typed nil, because then we'd be returning the
// result of MakeInterface.
return false
case *ir.ChangeInterface:
return do(v.X, seen)
case *ir.Extract:
call, ok := v.Tuple.(*ir.Call)
if !ok {
// We only care about extracts of function results. For
// everything else (e.g. channel receives and map
// lookups), we can either not deduce any information, or
// will see a MakeInterface.
return false
}
if callee := call.Call.StaticCallee(); callee != nil {
return impl(pass, callee, seenFns)&(1<<v.Index) != 0
} else {
// we don't know what function we're calling. no need
// to look at the signature, though. if it weren't an
// interface, we'd be seeing a MakeInterface
// instruction.
return false
}
case *ir.Call:
if callee := v.Call.StaticCallee(); callee != nil {
return impl(pass, callee, seenFns)&1 != 0
} else {
// we don't know what function we're calling. no need
// to look at the signature, though. if it weren't an
// interface, we'd be seeing a MakeInterface
// instruction.
return false
}
case *ir.Sigma:
iff, ok := v.From.Control().(*ir.If)
if !ok {
// give up
return false
}
binop, ok := iff.Cond.(*ir.BinOp)
if !ok {
// give up
return false
}
if (binop.X == v.X && isUntypedNil(binop.Y)) || (isUntypedNil(binop.X) && binop.Y == v.X) {
op := binop.Op
if v.From.Succs[0] != v.Block() {
// we're in the false branch, negate op
switch op {
case token.EQL:
op = token.NEQ
case token.NEQ:
op = token.EQL
default:
panic(fmt.Sprintf("internal error: unhandled token %v", op))
}
}
switch op {
case token.EQL:
// returned value equals untyped nil
return false
case token.NEQ:
// returned value does not equal untyped nil
return true
default:
panic(fmt.Sprintf("internal error: unhandled token %v", op))
}
}
// TODO(dh): handle comparison with typed nil
// give up
return false
case *ir.Phi:
for _, pv := range v.Edges {
if !do(pv, seen) {
return false
}
}
return true
case *ir.MakeInterface:
terms, err := typeparams.NormalTerms(v.X.Type())
if len(terms) == 0 || err != nil {
// Type is a type parameter with no type terms (or we couldn't determine the terms). Such a type
// _can_ be nil when put in an interface value.
//
// There is no instruction that can create a guaranteed non-nil instance of a type parameter without
// type constraints, so we return false right away, without checking v.X's typedness.
return false
}
return true
case *ir.TypeAssert:
// type assertions fail for untyped nils. Either we have a
// single lhs and the type assertion succeeds or panics,
// or we have two lhs and we'll return Extract instead.
return true
case *ir.ChangeType:
// we'll only see interface->interface conversions, which
// don't tell us anything about the nilness.
return false
case *ir.MapLookup, *ir.Index, *ir.Recv, *ir.Parameter, *ir.Load, *ir.Field:
// All other instructions that tell us nothing about the
// typedness of interface values.
return false
default:
panic(fmt.Sprintf("internal error: unhandled type %T", v))
}
}
ret := fn.Exit.Control().(*ir.Return)
for i, v := range ret.Results {
typ := fn.Signature.Results().At(i).Type()
if _, ok := typ.Underlying().(*types.Interface); ok && !typeparams.IsTypeParam(typ) {
if do(v, map[ir.Value]struct{}{}) {
out |= 1 << i
}
}
}
return out
}

View File

@@ -0,0 +1,221 @@
// Package lint provides abstractions on top of go/analysis.
// These abstractions add extra information to analyzes, such as structured documentation and severities.
package lint
import (
"fmt"
"go/ast"
"go/token"
"strings"
"golang.org/x/tools/go/analysis"
"honnef.co/go/tools/analysis/facts/tokenfile"
)
// Analyzer wraps a go/analysis.Analyzer and provides structured documentation.
type Analyzer struct {
// The analyzer's documentation. Unlike go/analysis.Analyzer.Doc,
// this field is structured, providing access to severity, options
// etc.
Doc *RawDocumentation
Analyzer *analysis.Analyzer
}
func InitializeAnalyzer(a *Analyzer) *Analyzer {
a.Analyzer.Doc = a.Doc.Compile().String()
a.Analyzer.URL = "https://staticcheck.dev/docs/checks/#" + a.Analyzer.Name
a.Analyzer.Requires = append(a.Analyzer.Requires, tokenfile.Analyzer)
return a
}
// Severity describes the severity of diagnostics reported by an analyzer.
type Severity int
const (
SeverityNone Severity = iota
SeverityError
SeverityDeprecated
SeverityWarning
SeverityInfo
SeverityHint
)
// MergeStrategy sets how merge mode should behave for diagnostics of an analyzer.
type MergeStrategy int
const (
MergeIfAny MergeStrategy = iota
MergeIfAll
)
type RawDocumentation struct {
Title string
Text string
Before string
After string
Since string
NonDefault bool
Options []string
Severity Severity
MergeIf MergeStrategy
}
type Documentation struct {
Title string
Text string
TitleMarkdown string
TextMarkdown string
Before string
After string
Since string
NonDefault bool
Options []string
Severity Severity
MergeIf MergeStrategy
}
func (doc RawDocumentation) Compile() *Documentation {
return &Documentation{
Title: strings.TrimSpace(stripMarkdown(doc.Title)),
Text: strings.TrimSpace(stripMarkdown(doc.Text)),
TitleMarkdown: strings.TrimSpace(toMarkdown(doc.Title)),
TextMarkdown: strings.TrimSpace(toMarkdown(doc.Text)),
Before: strings.TrimSpace(doc.Before),
After: strings.TrimSpace(doc.After),
Since: doc.Since,
NonDefault: doc.NonDefault,
Options: doc.Options,
Severity: doc.Severity,
MergeIf: doc.MergeIf,
}
}
func toMarkdown(s string) string {
return strings.NewReplacer(`\'`, "`", `\"`, "`").Replace(s)
}
func stripMarkdown(s string) string {
return strings.NewReplacer(`\'`, "", `\"`, "'").Replace(s)
}
func (doc *Documentation) Format(metadata bool) string {
return doc.format(false, metadata)
}
func (doc *Documentation) FormatMarkdown(metadata bool) string {
return doc.format(true, metadata)
}
func (doc *Documentation) format(markdown bool, metadata bool) string {
b := &strings.Builder{}
if markdown {
fmt.Fprintf(b, "%s\n\n", doc.TitleMarkdown)
if doc.Text != "" {
fmt.Fprintf(b, "%s\n\n", doc.TextMarkdown)
}
} else {
fmt.Fprintf(b, "%s\n\n", doc.Title)
if doc.Text != "" {
fmt.Fprintf(b, "%s\n\n", doc.Text)
}
}
if doc.Before != "" {
fmt.Fprintln(b, "Before:")
fmt.Fprintln(b, "")
for line := range strings.SplitSeq(doc.Before, "\n") {
fmt.Fprint(b, " ", line, "\n")
}
fmt.Fprintln(b, "")
fmt.Fprintln(b, "After:")
fmt.Fprintln(b, "")
for line := range strings.SplitSeq(doc.After, "\n") {
fmt.Fprint(b, " ", line, "\n")
}
fmt.Fprintln(b, "")
}
if metadata {
fmt.Fprint(b, "Available since\n ")
if doc.Since == "" {
fmt.Fprint(b, "unreleased")
} else {
fmt.Fprintf(b, "%s", doc.Since)
}
if doc.NonDefault {
fmt.Fprint(b, ", non-default")
}
fmt.Fprint(b, "\n")
if len(doc.Options) > 0 {
fmt.Fprintf(b, "\nOptions\n")
for _, opt := range doc.Options {
fmt.Fprintf(b, " %s", opt)
}
fmt.Fprint(b, "\n")
}
}
return b.String()
}
func (doc *Documentation) String() string {
return doc.Format(true)
}
// ExhaustiveTypeSwitch panics when called. It can be used to ensure
// that type switches are exhaustive.
func ExhaustiveTypeSwitch(v any) {
panic(fmt.Sprintf("internal error: unhandled case %T", v))
}
// A directive is a comment of the form '//lint:<command>
// [arguments...]'. It represents instructions to the static analysis
// tool.
type Directive struct {
Command string
Arguments []string
Directive *ast.Comment
Node ast.Node
}
func parseDirective(s string) (cmd string, args []string) {
if !strings.HasPrefix(s, "//lint:") {
return "", nil
}
s = strings.TrimPrefix(s, "//lint:")
fields := strings.Split(s, " ")
return fields[0], fields[1:]
}
// ParseDirectives extracts all directives from a list of Go files.
func ParseDirectives(files []*ast.File, fset *token.FileSet) []Directive {
var dirs []Directive
for _, f := range files {
// OPT(dh): in our old code, we skip all the comment map work if we
// couldn't find any directives, benchmark if that's actually
// worth doing
cm := ast.NewCommentMap(fset, f, f.Comments)
for node, cgs := range cm {
for _, cg := range cgs {
for _, c := range cg.List {
if !strings.HasPrefix(c.Text, "//lint:") {
continue
}
cmd, args := parseDirective(c.Text)
d := Directive{
Command: cmd,
Arguments: args,
Directive: c,
Node: node,
}
dirs = append(dirs, d)
}
}
}
}
return dirs
}

View File

@@ -0,0 +1,281 @@
package report
import (
"bytes"
"fmt"
"go/ast"
"go/format"
"go/token"
"go/version"
"path/filepath"
"strconv"
"strings"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/facts/generated"
"honnef.co/go/tools/go/ast/astutil"
"golang.org/x/tools/go/analysis"
)
type Options struct {
ShortRange bool
FilterGenerated bool
Fixes []analysis.SuggestedFix
Related []analysis.RelatedInformation
MinimumLanguageVersion string
MaximumLanguageVersion string
MinimumStdlibVersion string
MaximumStdlibVersion string
}
type Option func(*Options)
func ShortRange() Option {
return func(opts *Options) {
opts.ShortRange = true
}
}
func FilterGenerated() Option {
return func(opts *Options) {
opts.FilterGenerated = true
}
}
func Fixes(fixes ...analysis.SuggestedFix) Option {
return func(opts *Options) {
opts.Fixes = append(opts.Fixes, fixes...)
}
}
func Related(node Positioner, message string) Option {
return func(opts *Options) {
pos, end, ok := getRange(node, opts.ShortRange)
if !ok {
return
}
r := analysis.RelatedInformation{
Pos: pos,
End: end,
Message: message,
}
opts.Related = append(opts.Related, r)
}
}
func MinimumLanguageVersion(vers string) Option {
return func(opts *Options) { opts.MinimumLanguageVersion = vers }
}
func MaximumLanguageVersion(vers string) Option {
return func(opts *Options) { opts.MinimumLanguageVersion = vers }
}
func MinimumStdlibVersion(vers string) Option {
return func(opts *Options) { opts.MinimumStdlibVersion = vers }
}
func MaximumStdlibVersion(vers string) Option {
return func(opts *Options) { opts.MaximumStdlibVersion = vers }
}
type Positioner interface {
Pos() token.Pos
}
type fullPositioner interface {
Pos() token.Pos
End() token.Pos
}
type sourcer interface {
Source() ast.Node
}
// shortRange returns the position and end of the main component of an
// AST node. For nodes that have no body, the short range is identical
// to the node's Pos and End. For nodes that do have a body, the short
// range excludes the body.
func shortRange(node ast.Node) (pos, end token.Pos) {
switch node := node.(type) {
case *ast.File:
return node.Pos(), node.Name.End()
case *ast.CaseClause:
return node.Pos(), node.Colon + 1
case *ast.CommClause:
return node.Pos(), node.Colon + 1
case *ast.DeferStmt:
return node.Pos(), node.Defer + token.Pos(len("defer"))
case *ast.ExprStmt:
return shortRange(node.X)
case *ast.ForStmt:
if node.Post != nil {
return node.For, node.Post.End()
} else if node.Cond != nil {
return node.For, node.Cond.End()
} else if node.Init != nil {
// +1 to catch the semicolon, for gofmt'ed code
return node.Pos(), node.Init.End() + 1
} else {
return node.Pos(), node.For + token.Pos(len("for"))
}
case *ast.FuncDecl:
return node.Pos(), node.Type.End()
case *ast.FuncLit:
return node.Pos(), node.Type.End()
case *ast.GoStmt:
if _, ok := astutil.Unparen(node.Call.Fun).(*ast.FuncLit); ok {
return node.Pos(), node.Go + token.Pos(len("go"))
} else {
return node.Pos(), node.End()
}
case *ast.IfStmt:
return node.Pos(), node.Cond.End()
case *ast.RangeStmt:
return node.Pos(), node.X.End()
case *ast.SelectStmt:
return node.Pos(), node.Pos() + token.Pos(len("select"))
case *ast.SwitchStmt:
if node.Tag != nil {
return node.Pos(), node.Tag.End()
} else if node.Init != nil {
// +1 to catch the semicolon, for gofmt'ed code
return node.Pos(), node.Init.End() + 1
} else {
return node.Pos(), node.Pos() + token.Pos(len("switch"))
}
case *ast.TypeSwitchStmt:
return node.Pos(), node.Assign.End()
default:
return node.Pos(), node.End()
}
}
func HasRange(node Positioner) bool {
// we don't know if getRange will be called with shortRange set to
// true, so make sure that both work.
_, _, ok := getRange(node, false)
if !ok {
return false
}
_, _, ok = getRange(node, true)
return ok
}
func getRange(node Positioner, short bool) (pos, end token.Pos, ok bool) {
switch n := node.(type) {
case sourcer:
s := n.Source()
if s == nil {
return 0, 0, false
}
if short {
p, e := shortRange(s)
return p, e, true
}
return s.Pos(), s.End(), true
case fullPositioner:
if short {
p, e := shortRange(n)
return p, e, true
}
return n.Pos(), n.End(), true
default:
return n.Pos(), token.NoPos, true
}
}
func Report(pass *analysis.Pass, node Positioner, message string, opts ...Option) {
cfg := &Options{}
for _, opt := range opts {
opt(cfg)
}
langVersion := code.LanguageVersion(pass, node)
stdlibVersion := code.StdlibVersion(pass, node)
if n := cfg.MaximumLanguageVersion; n != "" && version.Compare(n, langVersion) == -1 {
return
}
if n := cfg.MaximumStdlibVersion; n != "" && version.Compare(n, stdlibVersion) == -1 {
return
}
if n := cfg.MinimumLanguageVersion; n != "" && version.Compare(n, langVersion) == 1 {
return
}
if n := cfg.MinimumStdlibVersion; n != "" && version.Compare(n, stdlibVersion) == 1 {
return
}
file := DisplayPosition(pass.Fset, node.Pos()).Filename
if cfg.FilterGenerated {
m := pass.ResultOf[generated.Analyzer].(map[string]generated.Generator)
if _, ok := m[file]; ok {
return
}
}
pos, end, ok := getRange(node, cfg.ShortRange)
if !ok {
panic(fmt.Sprintf("no valid position for reporting node %v", node))
}
d := analysis.Diagnostic{
Pos: pos,
End: end,
Message: message,
SuggestedFixes: cfg.Fixes,
Related: cfg.Related,
}
pass.Report(d)
}
func Render(pass *analysis.Pass, x any) string {
var buf bytes.Buffer
if err := format.Node(&buf, pass.Fset, x); err != nil {
panic(err)
}
return buf.String()
}
func RenderArgs(pass *analysis.Pass, args []ast.Expr) string {
var ss []string
for _, arg := range args {
ss = append(ss, Render(pass, arg))
}
return strings.Join(ss, ", ")
}
func DisplayPosition(fset *token.FileSet, p token.Pos) token.Position {
if p == token.NoPos {
return token.Position{}
}
// Only use the adjusted position if it points to another Go file.
// This means we'll point to the original file for cgo files, but
// we won't point to a YACC grammar file.
pos := fset.PositionFor(p, false)
adjPos := fset.PositionFor(p, true)
if filepath.Ext(adjPos.Filename) == ".go" {
return adjPos
}
return pos
}
func Ordinal(n int) string {
suffix := "th"
if n < 10 || n > 20 {
switch n % 10 {
case 0:
suffix = "th"
case 1:
suffix = "st"
case 2:
suffix = "nd"
case 3:
suffix = "rd"
default:
suffix = "th"
}
}
return strconv.Itoa(n) + suffix
}