cmd/cloner, cmd/viewer: handle named map/slice types with Clone/View methods

The cloner and viewer code generators didn't handle named types
with basic underlying types (map/slice) that have their own Clone
or View methods. For example, a type like:

    type Map map[string]any
    func (m Map) Clone() Map { ... }
    func (m Map) View() MapView { ... }

When used as a struct field, the cloner would descend into the
underlying map[string]any and fail because it can't clone the any
(interface{}) value type. Similarly, the viewer would try to create
a MapFnOf view and fail.

Fix the cloner to check for a Clone method on the named type
before falling through to the underlying type handling.

Fix the viewer to check for a View method on named map/slice types,
so the type author can provide a purpose-built safe view that
doesn't leak raw any values. Named map/slice types without a View
method fall through to normal handling, which correctly rejects
types like map[string]any as unsupported.

Updates tailscale/corp#39502 (needed by tailscale/corp#39594)

Change-Id: Iaef0192a221e02b4b8e409c99ef8398090327744
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick
2026-04-05 22:56:53 +00:00
committed by Brad Fitzpatrick
parent 5a899e406d
commit 86f42ea87b
10 changed files with 440 additions and 6 deletions
+6
View File
@@ -129,6 +129,12 @@ func gen(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named) {
}
continue
}
// Named types with basic underlying types (map/slice) that
// have their own Clone method should use it directly.
if methodResultType(ft, "Clone") != nil {
writef("dst.%s = src.%s.Clone()", fname, fname)
continue
}
}
switch ft := ft.Underlying().(type) {
case *types.Slice:
+28
View File
@@ -154,6 +154,34 @@ func TestMapWithPointers(t *testing.T) {
}
}
func TestNamedMapContainer(t *testing.T) {
orig := &clonerex.NamedMapContainer{
Attrs: clonerex.NamedMap{
"str": "hello",
"num": int64(42),
"bool": true,
},
}
cloned := orig.Clone()
if !reflect.DeepEqual(orig, cloned) {
t.Errorf("Clone() = %v, want %v", cloned, orig)
}
// Mutate the cloned map to verify no aliasing.
cloned.Attrs["str"] = "modified"
if orig.Attrs["str"] == "modified" {
t.Errorf("Clone() aliased memory in Attrs: original was modified")
}
// Verify nil handling.
nilContainer := &clonerex.NamedMapContainer{}
nilClone := nilContainer.Clone()
if !reflect.DeepEqual(nilContainer, nilClone) {
t.Errorf("Clone() of nil Attrs = %v, want %v", nilClone, nilContainer)
}
}
func TestDeeplyNestedMap(t *testing.T) {
num := 123
orig := &clonerex.DeeplyNestedMap{
+22 -1
View File
@@ -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
//go:generate go run tailscale.com/cmd/cloner -clonefunc=true -type SliceContainer,InterfaceContainer,MapWithPointers,DeeplyNestedMap,NamedMapContainer
// Package clonerex is an example package for the cloner tool.
package clonerex
@@ -39,6 +39,27 @@ type MapWithPointers struct {
CloneInterface map[string]Cloneable
}
// NamedMap is a named map type with its own Clone method.
// This tests that the cloner uses the type's Clone method
// rather than trying to descend into the map's value type.
type NamedMap map[string]any
func (m NamedMap) Clone() NamedMap {
if m == nil {
return nil
}
m2 := make(NamedMap, len(m))
for k, v := range m {
m2[k] = v
}
return m2
}
// NamedMapContainer has a field whose type is a named map with a Clone method.
type NamedMapContainer struct {
Attrs NamedMap
}
// DeeplyNestedMap tests arbitrary depth of map nesting (3+ levels)
type DeeplyNestedMap struct {
ThreeLevels map[string]map[string]map[string]int
+27 -1
View File
@@ -159,9 +159,26 @@ var _DeeplyNestedMapCloneNeedsRegeneration = DeeplyNestedMap(struct {
FourLevels map[string]map[string]map[string]map[string]*SliceContainer
}{})
// Clone makes a deep copy of NamedMapContainer.
// The result aliases no memory with the original.
func (src *NamedMapContainer) Clone() *NamedMapContainer {
if src == nil {
return nil
}
dst := new(NamedMapContainer)
*dst = *src
dst.Attrs = src.Attrs.Clone()
return dst
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _NamedMapContainerCloneNeedsRegeneration = NamedMapContainer(struct {
Attrs NamedMap
}{})
// Clone duplicates src into dst and reports whether it succeeded.
// To succeed, <src, dst> must be of types <*T, *T> or <*T, **T>,
// where T is one of SliceContainer,InterfaceContainer,MapWithPointers,DeeplyNestedMap.
// where T is one of SliceContainer,InterfaceContainer,MapWithPointers,DeeplyNestedMap,NamedMapContainer.
func Clone(dst, src any) bool {
switch src := src.(type) {
case *SliceContainer:
@@ -200,6 +217,15 @@ func Clone(dst, src any) bool {
*dst = src.Clone()
return true
}
case *NamedMapContainer:
switch dst := dst.(type) {
case *NamedMapContainer:
*dst = *src.Clone()
return true
case **NamedMapContainer:
*dst = src.Clone()
return true
}
}
return false
}