internal/refactor/inline: make fact encoding deterministic The gob encoding of an analysis fact must be deterministic (see the "Modular analysis with Facts" section of golang.org/x/tools/go/analysis). The inline callee fact was encoded non-deterministically because it derived data from the randomized iteration order of Go maps. This is now fixed, and a test asserts that repeatedly analyzing and encoding a callee produces byte-identical output. Fixes golang/go#80237 Change-Id: I6fe93314fe841f48013e3f02f147fd5455a74b0f Reviewed-on: https://go-review.googlesource.com/c/tools/+/802700 Reviewed-by: Carlos Amedee <carlos@golang.org> Auto-Submit: Alan Donovan <adonovan@google.com> LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Alan Donovan <adonovan@google.com>
diff --git a/internal/moremaps/maps.go b/internal/moremaps/maps.go index e4ebee1..a1bae07 100644 --- a/internal/moremaps/maps.go +++ b/internal/moremaps/maps.go
@@ -90,3 +90,27 @@ delete(m, k) return pre != len(m) } + +// Entry is a key-value pair obtained from a map. +type Entry[K comparable, V any] struct { + Key K + Value V +} + +// Entries returns a new unordered array of the entries of a map. +func Entries[M ~map[K]V, K comparable, V any](m M) []Entry[K, V] { + entries := make([]Entry[K, V], 0, len(m)) + for k, v := range m { + entries = append(entries, Entry[K, V]{k, v}) + } + return entries +} + +// FromEntries returns a new map into which the entries have been inserted in order. +func FromEntries[K comparable, V any](entries []Entry[K, V]) map[K]V { + m := make(map[K]V, len(entries)) + for _, e := range entries { + m[e.Key] = e.Value + } + return m +}
diff --git a/internal/refactor/inline/callee.go b/internal/refactor/inline/callee.go index 68e2844..313e0f7 100644 --- a/internal/refactor/inline/callee.go +++ b/internal/refactor/inline/callee.go
@@ -8,6 +8,7 @@ import ( "bytes" + "cmp" "encoding/gob" "fmt" "go/ast" @@ -18,6 +19,7 @@ "strings" "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/internal/moremaps" "golang.org/x/tools/internal/typeparams" "golang.org/x/tools/internal/typesinternal" ) @@ -885,6 +887,34 @@ return s } +var ( + _ gob.GobEncoder = (*shadowMap)(nil) + _ gob.GobDecoder = (*shadowMap)(nil) +) + +// GobEncode implements gob.GobEncoder, encoding the map's entries in a +// deterministic order so that serialized facts are stable. +func (s *shadowMap) GobEncode() ([]byte, error) { + entries := moremaps.Entries(*s) + slices.SortFunc(entries, func(x, y moremaps.Entry[string, int]) int { + return cmp.Compare(x.Key, y.Key) + }) + var out bytes.Buffer + if err := gob.NewEncoder(&out).Encode(entries); err != nil { + return nil, err + } + return out.Bytes(), nil +} + +func (s *shadowMap) GobDecode(data []byte) error { + var entries []moremaps.Entry[string, int] + if err := gob.NewDecoder(bytes.NewReader(data)).Decode(&entries); err != nil { + return err + } + *s = moremaps.FromEntries(entries) + return nil +} + // fieldObjs returns a map of each types.Object defined by the given signature // to its index in the parameter list. Parameters with missing or blank name // are skipped.
diff --git a/internal/refactor/inline/calleefx.go b/internal/refactor/inline/calleefx.go index 001bf61..6dcf0b9 100644 --- a/internal/refactor/inline/calleefx.go +++ b/internal/refactor/inline/calleefx.go
@@ -10,6 +10,7 @@ "go/ast" "go/token" "go/types" + "slices" "golang.org/x/tools/internal/typesinternal" ) @@ -71,14 +72,20 @@ // unreferenced by the function body). This lets us // not bother implementing the complete traversal into // control structures. - // - // TODO(adonovan): add them in a deterministic order. - // (This is not a bug but determinism is good.) - for _, pinfo := range paramInfos { + + // Sort params by Index for determinism + sortedParams := make([]*types.Var, 0, len(paramInfos)) + for obj, pinfo := range paramInfos { if !pinfo.IsResult && len(pinfo.Refs) > 0 { - effect(pinfo.Index) + sortedParams = append(sortedParams, obj) } } + slices.SortFunc(sortedParams, func(a, b *types.Var) int { + return paramInfos[a].Index - paramInfos[b].Index + }) + for _, obj := range sortedParams { + effect(paramInfos[obj].Index) + } } var visitExpr func(n ast.Expr)
diff --git a/internal/refactor/inline/falcon.go b/internal/refactor/inline/falcon.go index 037d33b..884a807 100644 --- a/internal/refactor/inline/falcon.go +++ b/internal/refactor/inline/falcon.go
@@ -13,6 +13,7 @@ "go/format" "go/token" "go/types" + "slices" "strconv" "strings" @@ -124,11 +125,20 @@ // type mapping st.int = st.typename(types.Typ[types.Int]) st.any = "interface{}" // don't use "any" as it may be shadowed - for obj, info := range st.params { + + // Sort params by Index for determinism + sortedParams := make([]*types.Var, 0, len(st.params)) + for obj := range st.params { if isBasic(obj.Type(), types.IsConstType) { - info.FalconType = st.typename(obj.Type()) + sortedParams = append(sortedParams, obj) } } + slices.SortFunc(sortedParams, func(a, b *types.Var) int { + return st.params[a].Index - st.params[b].Index + }) + for _, obj := range sortedParams { + st.params[obj].FalconType = st.typename(obj.Type()) + } st.stmt(st.decl.Body)