From 3872880617733ca92d8d698dcc692ad0fd63e1f6 Mon Sep 17 00:00:00 2001 From: Andrew Lytvynov Date: Fri, 10 Jul 2026 08:56:49 -0700 Subject: [PATCH] cmd/cloner: handle named slices as map values (#20387) Previously cloner only handled literal slices for values, like `map[string][]int`. This adds support for named types with an underlying type of slice, like `map[string]IntSlice` with `type IntSlice []int`. Updates tailscale/corp#44077 Signed-off-by: Andrew Lytvynov --- cmd/cloner/cloner.go | 2 +- cmd/cloner/cloner_test.go | 10 ++++++++ cmd/cloner/clonerex/clonerex.go | 11 ++++++++- cmd/cloner/clonerex/clonerex_clone.go | 33 ++++++++++++++++++++++++++- 4 files changed, 53 insertions(+), 3 deletions(-) diff --git a/cmd/cloner/cloner.go b/cmd/cloner/cloner.go index 8b4cacf7a..2894c862d 100644 --- a/cmd/cloner/cloner.go +++ b/cmd/cloner/cloner.go @@ -169,7 +169,7 @@ func gen(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named) { writef("}") case *types.Map: elem := ft.Elem() - if sliceType, isSlice := elem.(*types.Slice); isSlice { + if sliceType, isSlice := elem.Underlying().(*types.Slice); isSlice { n := it.QualifiedName(sliceType.Elem()) writef("if dst.%s != nil {", fname) writef("\tdst.%s = map[%s]%s{}", fname, it.QualifiedName(ft.Key()), it.QualifiedName(elem)) diff --git a/cmd/cloner/cloner_test.go b/cmd/cloner/cloner_test.go index f8beb4a88..04670e1f5 100644 --- a/cmd/cloner/cloner_test.go +++ b/cmd/cloner/cloner_test.go @@ -283,3 +283,13 @@ func TestDeeplyNestedMap(t *testing.T) { t.Errorf("Clone() aliased FourLevels map: new nested key appeared in original") } } + +func TestMapWithNamedSliceValues(t *testing.T) { + orig := &clonerex.MapWithNamedSliceValues{ + M: map[string]clonerex.NamedSlice{"k": {"foo", "bar"}}, + } + cloned := orig.Clone() + if diff := cmp.Diff(orig, cloned); diff != "" { + t.Errorf("Clone() mismatch (-orig +cloned):\n%s", diff) + } +} diff --git a/cmd/cloner/clonerex/clonerex.go b/cmd/cloner/clonerex/clonerex.go index 41626d3ae..e38aec199 100644 --- a/cmd/cloner/clonerex/clonerex.go +++ b/cmd/cloner/clonerex/clonerex.go @@ -1,7 +1,7 @@ // Copyright (c) Tailscale Inc & contributors // SPDX-License-Identifier: BSD-3-Clause -//go:generate go run tailscale.com/cmd/cloner -clonefunc=true -type SliceContainer,InterfaceContainer,MapWithPointers,DeeplyNestedMap,NamedMapContainer,MapSlicePointerContainer +//go:generate go run tailscale.com/cmd/cloner -clonefunc=true -type SliceContainer,InterfaceContainer,MapWithPointers,DeeplyNestedMap,NamedMapContainer,MapSlicePointerContainer,MapWithNamedSliceValues // Package clonerex is an example package for the cloner tool. package clonerex @@ -72,3 +72,12 @@ type DeeplyNestedMap struct { ThreeLevels map[string]map[string]map[string]int FourLevels map[string]map[string]map[string]map[string]*SliceContainer } + +// MapWithNamedSliceValues has a map with a named slice type for values. This +// tests that the generator treats these values like any other slice and not a +// struct. +type MapWithNamedSliceValues struct { + M map[string]NamedSlice +} + +type NamedSlice []string diff --git a/cmd/cloner/clonerex/clonerex_clone.go b/cmd/cloner/clonerex/clonerex_clone.go index 9a4413177..dbe9f5387 100644 --- a/cmd/cloner/clonerex/clonerex_clone.go +++ b/cmd/cloner/clonerex/clonerex_clone.go @@ -209,9 +209,31 @@ func (src *MapSlicePointerContainer) Clone() *MapSlicePointerContainer { Routes map[string][]*SliceContainer }{}) +// Clone makes a deep copy of MapWithNamedSliceValues. +// The result aliases no memory with the original. +func (src *MapWithNamedSliceValues) Clone() *MapWithNamedSliceValues { + if src == nil { + return nil + } + dst := new(MapWithNamedSliceValues) + *dst = *src + if dst.M != nil { + dst.M = map[string]NamedSlice{} + for k := range src.M { + dst.M[k] = append([]string{}, src.M[k]...) + } + } + return dst +} + +// A compilation failure here means this code must be regenerated, with the command at the top of this file. +var _MapWithNamedSliceValuesCloneNeedsRegeneration = MapWithNamedSliceValues(struct { + M map[string]NamedSlice +}{}) + // Clone duplicates src into dst and reports whether it succeeded. // To succeed, must be of types <*T, *T> or <*T, **T>, -// where T is one of SliceContainer,InterfaceContainer,MapWithPointers,DeeplyNestedMap,NamedMapContainer,MapSlicePointerContainer. +// where T is one of SliceContainer,InterfaceContainer,MapWithPointers,DeeplyNestedMap,NamedMapContainer,MapSlicePointerContainer,MapWithNamedSliceValues. func Clone(dst, src any) bool { switch src := src.(type) { case *SliceContainer: @@ -268,6 +290,15 @@ func Clone(dst, src any) bool { *dst = src.Clone() return true } + case *MapWithNamedSliceValues: + switch dst := dst.(type) { + case *MapWithNamedSliceValues: + *dst = *src.Clone() + return true + case **MapWithNamedSliceValues: + *dst = src.Clone() + return true + } } return false }