package facts import ( "go/types" "reflect" "golang.org/x/tools/go/analysis" "honnef.co/go/tools/functions" "honnef.co/go/tools/internal/passes/buildir" "honnef.co/go/tools/ir" ) type IsPure struct{} func (*IsPure) AFact() {} func (d *IsPure) String() string { return "is pure" } type PurityResult map[*types.Func]*IsPure var Purity = &analysis.Analyzer{ Name: "fact_purity", Doc: "Mark pure functions", Run: purity, Requires: []*analysis.Analyzer{buildir.Analyzer}, FactTypes: []analysis.Fact{(*IsPure)(nil)}, ResultType: reflect.TypeOf(PurityResult{}), } 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": {}, } func purity(pass *analysis.Pass) (interface{}, 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 functions.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 } 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 _, ok := param.Type().Underlying().(*types.Basic); !ok { 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 } 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: return false case *ir.FieldAddr: return false case *ir.Alloc: return false case *ir.Load: return false } } } return true } for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs { check(fn) } out := PurityResult{} for _, fact := range pass.AllObjectFacts() { out[fact.Object.(*types.Func)] = fact.Fact.(*IsPure) } return out, nil }