Update go version

This commit is contained in:
dwrz
2026-08-21 10:23:37 +00:00
parent 78248a6145
commit c2e2d9ea02
466 changed files with 67766 additions and 2881 deletions

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,830 @@
package nilness
import (
"fmt"
"go/constant"
"go/token"
"go/types"
"reflect"
"slices"
"strings"
"honnef.co/go/tools/analysis/dfa"
"honnef.co/go/tools/analysis/dfa/dense"
"honnef.co/go/tools/go/ir"
"honnef.co/go/tools/go/types/typeutil"
"honnef.co/go/tools/internal/passes/buildir"
"golang.org/x/exp/typeparams"
"golang.org/x/tools/go/analysis"
)
// TODO(dh): The analysis is currently entirely forward, which means that for
//
// x := s[:0]
// y := s[:1]
// z := s[:0]
//
// x will have MaybeNil at every program point and s will have MaybeNil before
// execution of y, even though executing y without panicing tells us that s has
// been non-nil for all 3 instructions.
type nilnessFact struct {
Rets []ValueNilness
}
func (*nilnessFact) AFact() {}
func (fact *nilnessFact) String() string {
return fmt.Sprintf("nilness: %v", fact.Rets)
}
type ValueNilness struct {
// Undefined for non-interface values.
// For interface values, whether the stored value may be nil.
// Even when Outer == MaybeNil, Inner may still offer precise information
// for the cases when Outer is dynamically not nil. For example, {NeverNil,
// MaybeNil} states that the interface value might be nil, but if it isn't,
// it will definitely contain a non-nil value.
Inner Nilness
// For non-interface values, whether the value may be nil.
// For interface values, whether the interface value may be nil.
Outer Nilness
}
type Result struct {
m map[*types.Func][]ValueNilness
}
var Analysis = &analysis.Analyzer{
Name: "nilness",
Doc: "Annotates return values with their nilness",
Run: run,
Requires: []*analysis.Analyzer{buildir.Analyzer},
FactTypes: []analysis.Fact{(*nilnessFact)(nil)},
ResultType: reflect.TypeFor[*Result](),
}
// Nilness returns nilness information for return value ret of fn.
func (r *Result) Nilness(fn *types.Func, ret int) ValueNilness {
typ := fn.Type().(*types.Signature).Results().At(ret).Type()
if !typeutil.IsPointerLike(typ) {
return ValueNilness{Outer: NeverNil}
}
if len(r.m[fn]) == 0 {
return ValueNilness{Inner: MaybeNil, Outer: MaybeNil}
}
return normalize(r.m[fn][ret], typ)
}
func normalize(v ValueNilness, typ types.Type) ValueNilness {
if v.Inner == 0 || !types.IsInterface(typ) {
v.Inner = MaybeNil
}
if v.Outer == 0 {
v.Outer = MaybeNil
}
return v
}
func run(pass *analysis.Pass) (any, error) {
seen := map[*ir.Function]struct{}{}
out := &Result{
m: map[*types.Func][]ValueNilness{},
}
// TODO(dh): instead of recursion and giving up on mutual recursion, we
// should compute the DFA over the call graph, at least until we have
// proper function summaries.
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.(*nilnessFact).Rets
}
return out, nil
}
type Nilness uint8
const (
// The value is never nil.
NeverNil Nilness = iota + 1
// The value is always nil.
AlwaysNil
// The value might be nil, but only because of the value of a global
// variable.
MaybeNilGlobal
// The value might be nil.
MaybeNil
)
func (n Nilness) String() string {
switch n {
case 0:
return "NoNilness"
case NeverNil:
return "NeverNil"
case AlwaysNil:
return "AlwaysNil"
case MaybeNilGlobal:
return "MaybeNilGlobal"
case MaybeNil:
return "MaybeNil"
default:
return "InvalidNilness"
}
}
type state struct {
cloned bool
m []ValueNilness
n numbering
}
func (s *state) get(v ir.Value) ValueNilness {
if !typeutil.IsPointerLike(v.Type()) {
// All non-pointer-like types are always {_ NeverNil}.
return ValueNilness{Outer: NeverNil}
}
num := s.n.number(v)
if num < len(s.m) {
return s.m[num]
}
switch v.(type) {
case *ir.Parameter:
return ValueNilness{Inner: MaybeNil, Outer: MaybeNil}
case *ir.Builtin:
return ValueNilness{Outer: NeverNil}
case *ir.FreeVar:
return ValueNilness{Inner: MaybeNil, Outer: MaybeNil}
case *ir.Function:
return ValueNilness{Outer: NeverNil}
case *ir.Global:
// Globals are addresses, not the values stored in them. The addresses
// cannot be nil.
return ValueNilness{Outer: NeverNil}
}
return lattice{}.Ident()
}
func (s *state) set(key ir.Value, value ValueNilness) {
if !typeutil.IsPointerLike(key.Type()) {
// No point in recording state for non-pointer-like types. They're
// always {_ NeverNil}.
return
}
if value == (lattice{}.Ident()) {
// No point in storing the default value.
return
}
num := s.n.number(key)
if !s.cloned {
if num < len(s.m) && s.m[num] == value {
// Don't clone if the value already matches.
return
}
s.cloned = true
s.m = slices.Clone(s.m)
}
if num >= len(s.m) {
s.m = append(s.m, make([]ValueNilness, num-len(s.m)+1)...)
}
s.m[num] = value
}
func (s *state) setInner(key ir.Value, value Nilness) {
if !typeutil.IsPointerLike(key.Type()) {
return
}
if value == (lattice{}.Ident().Inner) {
return
}
num := s.n.number(key)
if !s.cloned {
if num < len(s.m) && s.m[num].Inner == value {
// Don't clone if the value already matches.
return
}
s.cloned = true
s.m = slices.Clone(s.m)
}
if num >= len(s.m) {
dflt := s.get(key)
s.m = append(s.m, make([]ValueNilness, num-len(s.m)+1)...)
s.m[num] = dflt
}
v := s.m[num]
v.Inner = value
s.m[num] = v
}
func (s *state) setOuter(key ir.Value, value Nilness) {
if !typeutil.IsPointerLike(key.Type()) {
return
}
if value == (lattice{}).Ident().Outer {
return
}
num := s.n.number(key)
if !s.cloned {
if num < len(s.m) && s.m[num].Outer == value {
// Don't clone if the value already matches.
return
}
s.cloned = true
s.m = slices.Clone(s.m)
}
if num >= len(s.m) {
dflt := s.get(key)
s.m = append(s.m, make([]ValueNilness, num-len(s.m)+1)...)
s.m[num] = dflt
}
v := s.m[num]
v.Outer = value
s.m[num] = v
}
func defaultNilnessForSignature(pass *analysis.Pass, typ *types.Signature) []ValueNilness {
n := typ.Results().Len()
if n == 0 {
return nil
}
out := make([]ValueNilness, n)
for i := range n {
out[i] = defaultNilness(pass, typ.Results().At(i).Type())
}
return out
}
func defaultNilness(pass *analysis.Pass, typ types.Type) ValueNilness {
if typeutil.IsPointerLike(typ) {
// IsPointerLike handles type parameters with type sets, too.
return ValueNilness{MaybeNil, MaybeNil}
} else {
return ValueNilness{NeverNil, NeverNil}
}
}
func impl(pass *analysis.Pass, fn *ir.Function, seenFns map[*ir.Function]struct{}) []ValueNilness {
goto start
bailout:
return defaultNilnessForSignature(pass, fn.Signature)
start:
if fn.Signature.Results().Len() == 0 {
return nil
}
if fn.Object() == nil {
// TODO(dh): support closures
goto bailout
}
if fact := new(nilnessFact); pass.ImportObjectFact(fn.Object(), fact) {
return fact.Rets
}
if fn.Pkg != pass.ResultOf[buildir.Analyzer].(*buildir.IR).Pkg {
goto bailout
}
if fn.Blocks == nil {
goto bailout
}
if _, ok := seenFns[fn]; ok {
// break recursion
goto bailout
}
seenFns[fn] = struct{}{}
anyPointers := false
for ret := range fn.Signature.Results().Variables() {
if typeutil.IsPointerLike(ret.Type()) {
anyPointers = true
break
}
}
if !anyPointers {
goto bailout
}
n := numbering{}
processBlock := func(from, to *ir.BasicBlock, s state) state {
handleReturnValue := func(v ir.Value, call *ir.Call, idx int) {
typ := call.Common().Signature().Results().At(idx).Type()
if !typeutil.IsPointerLike(typ) {
s.setOuter(v, NeverNil)
return
}
if callee, ok := call.Call.Value.(*ir.Builtin); ok {
switch callee.Name() {
case "append":
// TODO(dh): if we knew that the varargs had non-zero
// length, we'd know that the resulting slice is non-nil.
switch an := s.get(call.Call.Args[0]).Outer; an {
case MaybeNil, MaybeNilGlobal, NeverNil:
s.setOuter(v, an)
case AlwaysNil:
s.setOuter(v, MaybeNil)
}
case "UnsafeSlice":
// If len is negative, or if ptr is nil and len is not
// zero, unsafe.Slice panics. This implies that a non-nil
// pointer cannot become nil, and vice versa.
s.set(v, s.get(call.Call.Args[0]))
case "UnsafeStringData":
// TODO(dh): if we had string length information we could
// return better information.
s.setOuter(v, MaybeNil)
case "UnsafeSliceData":
// When the slice is non-nil but has zero capacity, the
// returned pointer is still non-nil, so we don't have to
// worry about that.
s.set(v, s.get(call.Call.Args[0]))
case "UnsafeAdd":
// TODO(dh): a positive addend can never result in a nil pointer.
// Pointer arithmetic can turn nil pointers into non-nil
// ones and vice versa.
s.setOuter(v, MaybeNil)
case "ssa:deferstack":
s.setOuter(v, NeverNil)
case "ssa:wrapnilchk":
s.setOuter(v, NeverNil)
case "recover":
s.setOuter(v, MaybeNil)
default:
panic(fmt.Sprintf("internal error: unhandled builtin %s", callee.Name()))
}
return
}
callee := call.Common().StaticCallee()
if callee == nil {
// We don't know which function is being called.
s.set(v, ValueNilness{MaybeNil, MaybeNil})
return
}
calleeNilness := impl(pass, callee, seenFns)
if len(calleeNilness) > idx {
s.set(v, normalize(calleeNilness[idx], typ))
} else {
s.set(v, ValueNilness{MaybeNil, MaybeNil})
}
}
for _, instr := range from.Instrs {
// It is tempting to return early when instr is an ir.Value that
// doesn't have pointer type. However, instructions like ir.Load
// tell us something about the value being operated on.
switch v := instr.(type) {
case *ir.Convert:
s.set(v, s.get(v.X))
case *ir.SliceToArrayPointer:
// Go does not currently allow (*T)(s) where T is a type
// parameter with a type set consisting of array types, but it
// does allow (T)(s) where T is a type parameter with a type
// set consisting of pointers to array types.
allNonZero := typeutil.All(v.Type(), func(term *types.Term) bool {
ptr := term.Type().Underlying().(*types.Pointer).Elem()
return typeutil.All(ptr, func(innerTerm *types.Term) bool {
return innerTerm.Type().Underlying().(*types.Array).Len() != 0
})
})
if allNonZero {
// converting a slice to an array pointer of length > 0
// panics if the slice is nil
s.setOuter(v, NeverNil)
s.setOuter(v.X, NeverNil)
} else {
s.set(v, s.get(v.X))
}
case *ir.SliceToArray:
// Pretty much the same logic as SliceToArrayPointer, minus the
// pointer.
allNonZero := typeutil.All(v.Type(), func(term *types.Term) bool {
return term.Type().Underlying().(*types.Array).Len() != 0
})
if allNonZero {
// converting a slice to an array of length > 0
// panics if the slice is nil
s.setOuter(v.X, NeverNil)
}
case *ir.Slice:
if typeutil.All(v.X.Type(), typeutil.IsType[*types.Array]) {
// Slicing arrays never results in a nil slice.
s.setOuter(v, NeverNil)
continue
}
// checkBound returns true if one of the bounds (low, high,
// capacity) has non-zero value.
checkBound := func(v ir.Value) bool {
if v == nil {
return false
}
// TODO(dh): this is where integration with constant
// propagation and value range analysis would be useful.
if k, ok := v.(*ir.Const); ok {
kv, ok := constant.Int64Val(k.Value)
return !ok || kv != 0
}
return false
}
if checkBound(v.Low) || checkBound(v.High) || checkBound(v.Max) {
// One of the indices is non-zero, which means slicing can
// only succeed if the slicee is not nil.
s.setOuter(v, NeverNil)
s.setOuter(v.X, NeverNil)
} else {
// The new slice is as nilly as the slicee.
s.set(v, s.get(v.X))
}
case *ir.If:
cond := v.Cond
binop, ok := cond.(*ir.BinOp)
if !ok {
continue
}
isNil := func(v ir.Value) bool {
k, ok := v.(*ir.Const)
if !ok {
return false
}
return k.Value == nil
}
var target ir.Value
if isNil(binop.X) {
target = binop.Y
} else if isNil(binop.Y) {
target = binop.X
} else {
continue
}
op := binop.Op
if to != from.Succs[0] {
// 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:
s.set(target, ValueNilness{AlwaysNil, AlwaysNil})
case token.NEQ:
s.setOuter(target, NeverNil)
default:
panic(fmt.Sprintf("internal error: unhandled token %v", op))
}
// TODO(dh): also handle comparison of two non-nil values. The
// true branch of neverNil == nilly makes the nilly value neverNil.
case *ir.ChangeType:
s.set(v, s.get(v.X))
case *ir.MultiConvert:
s.set(v, s.get(v.X))
case *ir.Load:
if _, ok := v.X.(*ir.Global); ok {
s.setOuter(v, MaybeNilGlobal)
} else {
s.setOuter(v, MaybeNil)
}
s.setOuter(v.X, NeverNil)
case *ir.FieldAddr:
s.setOuter(v.X, NeverNil)
s.setOuter(v, NeverNil)
case *ir.IndexAddr:
s.setOuter(v.X, NeverNil)
s.setOuter(v, NeverNil)
case *ir.Alloc, *ir.MakeMap, *ir.MakeSlice, *ir.MakeClosure, *ir.MakeChan:
s.setOuter(v.(ir.Value), NeverNil)
case *ir.MapUpdate:
s.setOuter(v.Map, NeverNil)
case *ir.Store:
s.setOuter(v.Addr, NeverNil)
case ir.CallInstruction:
// go/defer/calling a nil function fatals/panics
if !v.Common().IsInvoke() {
s.setOuter(v.Common().Value, NeverNil)
}
_, ok := v.(*ir.Call)
if !ok {
// Defer and Go don't produce values
continue
}
if v.Common().Signature().Results().Len() != 1 {
// If the called function doesn't return any values then we
// don't care about it. If it has more than one return
// value, they'll be handled by Extract.
continue
}
handleReturnValue(v.(ir.Value), v.(*ir.Call), 0)
case *ir.Send:
s.setOuter(v.Chan, NeverNil)
case *ir.Recv:
s.setOuter(v.Chan, NeverNil)
s.set(v, ValueNilness{MaybeNil, MaybeNil})
case *ir.MakeInterface:
s.set(v, ValueNilness{
Inner: s.get(v.X).Outer,
Outer: NeverNil,
})
case *ir.ChangeInterface:
s.set(v, s.get(v.X))
case *ir.TypeAssert:
if !v.CommaOk {
// The interface value cannot have been nil, or the type
// assertion would have panicked.
s.setOuter(v.X, NeverNil)
if types.IsInterface(v.Type()) && !typeparams.IsTypeParam(v.Type()) {
// Type asserting to another interface doesn't succeed
// if the assertee was nil. It also results in a new
// interface value.
s.setOuter(v, NeverNil)
s.setInner(v, s.get(v.X).Inner)
} else {
// We've extracted the interface value's inner value.
s.setOuter(v, s.get(v.X).Inner)
}
} else {
// In a comma-ok type assertion, the return type is a
// tuple. There'll be Extract instructions getting the
// individual values, to which we'll attach the nilness
// info.
}
case *ir.TypeSwitch:
// Handled in Extract
case *ir.MapLookup:
if s.get(v.X).Outer == AlwaysNil {
s.set(v, ValueNilness{AlwaysNil, AlwaysNil})
} else {
s.set(v, ValueNilness{MaybeNil, MaybeNil})
}
case *ir.Field:
s.set(v.X, ValueNilness{NeverNil, NeverNil})
s.set(v, ValueNilness{MaybeNil, MaybeNil})
case *ir.Index:
s.set(v.X, ValueNilness{NeverNil, NeverNil})
s.set(v, ValueNilness{MaybeNil, MaybeNil})
case *ir.Extract:
switch tuple := v.Tuple.(type) {
case *ir.TypeAssert:
// When we get here, the type assertion used the comma-ok
// form, and we don't yet know anything about the result of
// the type assertion.
if v.Index == 0 {
s.set(v, ValueNilness{MaybeNil, MaybeNil})
}
// TODO(dh): We should set v's nilness in the true and
// false branches of checks on the ok value. However, ok can be
// used in arbitrary ways, and we're also not set up to handle
// relational facts (ok being true or false affects the value
// of the other Extract).
case *ir.Call:
handleReturnValue(v, tuple, v.Index)
case *ir.TypeSwitch:
if v.Index == 0 {
// Index 0 is an integer and not interesting.
continue
}
idx := v.Index - 1
if idx >= len(tuple.Conds) {
// Default branch
// If there is an untyped nil case, then being in the
// default branch tells us that the interface value
// isn't nil.
hasNil := slices.ContainsFunc(tuple.Conds, func(typ types.Type) bool {
if typ, ok := typ.(*types.Basic); ok && typ.Kind() == types.UntypedNil {
return true
}
return false
})
if hasNil {
s.setOuter(tuple.Tag, NeverNil)
} else {
s.setOuter(tuple.Tag, MaybeNil)
}
s.setOuter(v, s.get(tuple.Tag).Inner)
} else {
// There is no Extract for the 'untyped nil' case,
// which means that executing any Extract from a type
// switch implies that the switched-over value wasn't a
// nil interface value.
s.setOuter(tuple.Tag, NeverNil)
typ := tuple.Conds[idx]
if types.IsInterface(typ) && !typeparams.IsTypeParam(typ) {
// Succesfully type asserting to an interface type
// always produces a non-nil interface value.
s.setInner(v, s.get(tuple.Tag).Inner)
s.setOuter(v, NeverNil)
} else {
s.setOuter(v, s.get(tuple.Tag).Inner)
}
}
default:
s.set(v, ValueNilness{MaybeNil, MaybeNil})
}
case *ir.Select:
if v.Blocking && len(v.States) == 1 {
// If the select doesn't have a default branch and only has
// one state, that state's channel cannot have been nil if
// we finished execution the select.
s.setOuter(v.States[0].Chan, NeverNil)
}
case *ir.Jump, *ir.BlankStore, *ir.Phi,
*ir.Panic, *ir.Return, *ir.RunDefers, *ir.Unreachable, *ir.ConstantSwitch,
*ir.UnOp, *ir.BinOp, *ir.CompositeValue, *ir.Range, *ir.Next:
default:
posn := pass.Fset.PositionFor(v.Pos(), false)
panic(fmt.Sprintf("internal error: unhandled type %T at %s", v, posn))
}
}
return s
}
processPhis := func(b *ir.BasicBlock, i int, s state) state {
for _, instr := range b.Instrs {
if instr, ok := instr.(*ir.Phi); ok {
s.set(instr, s.get(instr.Edges[i]))
} else {
break
}
}
return s
}
// Populate default state for non-instruction values we encounter. We
// cannot defer this logic to state.get because control flow merges use
// simple merges of lattice values and won't know about value-specific
// defaults.
entrys := state{cloned: true, n: n}
for _, param := range fn.Params {
if typeutil.IsPointerLike(param.Type()) {
entrys.set(param, ValueNilness{Inner: MaybeNil, Outer: MaybeNil})
} else {
// We never track nilness for value types, so they don't have to be
// present in the entry state, either.
}
}
if strings.HasPrefix(fn.Synthetic, "bound method wrapper") {
// This is a bound method and the bound receiver might be nil
for _, fvar := range fn.FreeVars {
entrys.set(fvar, ValueNilness{Outer: MaybeNil})
}
} else {
// This is a closure, and closed over variables are allocs, which
// cannot be nil.
for _, fvar := range fn.FreeVars {
entrys.set(fvar, ValueNilness{Outer: NeverNil})
}
}
var ops []*ir.Value
for _, b := range fn.Blocks {
for _, instr := range b.Instrs {
ops = instr.Operands(ops[:0])
for _, pop := range ops {
if op, ok := (*pop).(*ir.Const); ok && typeutil.IsPointerLike(op.Type()) {
// The only constant pointer-like is nil.
entrys.set(op, ValueNilness{Inner: AlwaysNil, Outer: AlwaysNil})
}
}
}
}
res := dense.Forward[dfa.DenseMapLattice[ValueNilness, lattice]](
fn,
map[int][]ValueNilness{0: entrys.m},
func(fromID, toID int, in []ValueNilness) []ValueNilness {
from := fn.Blocks[fromID]
to := fn.Blocks[toID]
s := state{n: n, m: in}
s = processBlock(from, to, s)
i := slices.Index(to.Preds, from)
s = processPhis(to, i, s)
return s.m
},
)
retNilness := make([]ValueNilness, fn.Signature.Results().Len())
for b := range fn.Returns() {
ret := b.Control().(*ir.Return)
s := state{n: n, m: res.In(b.Index)}
s = processBlock(b, nil, s)
for i, res := range ret.Results {
retNilness[i] = lattice{}.Merge(retNilness[i], s.get(res))
}
}
interesting := false
for i := range retNilness {
typ := fn.Signature.Results().At(i).Type()
if !typeutil.IsPointerLike(typ) {
retNilness[i] = ValueNilness{NeverNil, NeverNil}
continue
}
retNilness[i] = normalize(retNilness[i], typ)
if retNilness[i] != (ValueNilness{MaybeNil, MaybeNil}) {
interesting = true
}
}
if interesting {
pass.ExportObjectFact(fn.Object(), &nilnessFact{retNilness})
}
return retNilness
}
type lattice struct{}
var _ dfa.Semilattice[ValueNilness] = lattice{}
// Equals implements [dfa.Semilattice].
func (l lattice) Equals(a, b ValueNilness) bool {
return a == b
}
// Ident implements [dfa.Semilattice].
func (l lattice) Ident() ValueNilness {
return ValueNilness{}
}
var latticeMerge = [5][5]Nilness{
0: {
0: 0,
NeverNil: NeverNil,
AlwaysNil: AlwaysNil,
MaybeNilGlobal: MaybeNilGlobal,
MaybeNil: MaybeNil,
},
NeverNil: {
0: NeverNil,
NeverNil: NeverNil,
AlwaysNil: MaybeNil,
MaybeNilGlobal: MaybeNilGlobal,
MaybeNil: MaybeNil,
},
AlwaysNil: {
0: AlwaysNil,
NeverNil: MaybeNil,
AlwaysNil: AlwaysNil,
MaybeNilGlobal: MaybeNil,
MaybeNil: MaybeNil,
},
MaybeNilGlobal: {
0: MaybeNilGlobal,
NeverNil: MaybeNilGlobal,
AlwaysNil: MaybeNil,
MaybeNilGlobal: MaybeNilGlobal,
MaybeNil: MaybeNil,
},
MaybeNil: {
0: MaybeNil,
NeverNil: MaybeNil,
AlwaysNil: MaybeNil,
MaybeNilGlobal: MaybeNil,
MaybeNil: MaybeNil,
},
}
// Merge implements [dfa.Semilattice].
func (l lattice) Merge(a, b ValueNilness) ValueNilness {
return ValueNilness{
Inner: latticeMerge[a.Inner][b.Inner],
Outer: latticeMerge[a.Outer][b.Outer],
}
}
type numbering map[ir.Value]int
func (n numbering) number(v ir.Value) int {
i, ok := n[v]
if !ok {
i = len(n)
n[v] = i
}
return i
}

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](),
}