mirror of
https://github.com/tailscale/tailscale.git
synced 2025-08-12 13:48:01 +00:00
cmd/viewer, types/views, util/codegen: add viewer support for custom container types
This adds support for container-like types such as Container[T] that don't explicitly specify a view type for T. Instead, a package implementing a container type should also implement and export a ContainerView[T, V] type and a ContainerViewOf(*Container[T]) ContainerView[T, V] function, which returns a view for the specified container, inferring the element view type V from the element type T. Updates #12736 Signed-off-by: Nick Khyl <nickk@tailscale.com>
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"reflect"
|
||||
"slices"
|
||||
|
||||
"go4.org/mem"
|
||||
@@ -111,6 +112,13 @@ type StructView[T any] interface {
|
||||
AsStruct() T
|
||||
}
|
||||
|
||||
// Cloner is any type that has a Clone function returning a deep-clone of the receiver.
|
||||
type Cloner[T any] interface {
|
||||
// Clone returns a deep-clone of the receiver.
|
||||
// It returns nil, when the receiver is nil.
|
||||
Clone() T
|
||||
}
|
||||
|
||||
// ViewCloner is any type that has had View and Clone funcs generated using
|
||||
// tailscale.com/cmd/viewer.
|
||||
type ViewCloner[T any, V StructView[T]] interface {
|
||||
@@ -555,3 +563,46 @@ func (m MapFn[K, T, V]) Range(f MapRangeFn[K, V]) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ContainsPointers reports whether T contains any pointers,
|
||||
// either explicitly or implicitly.
|
||||
// It has special handling for some types that contain pointers
|
||||
// that we know are free from memory aliasing/mutation concerns.
|
||||
func ContainsPointers[T any]() bool {
|
||||
return containsPointers(reflect.TypeFor[T]())
|
||||
}
|
||||
|
||||
func containsPointers(typ reflect.Type) bool {
|
||||
switch typ.Kind() {
|
||||
case reflect.Pointer, reflect.UnsafePointer:
|
||||
return true
|
||||
case reflect.Chan, reflect.Map, reflect.Slice:
|
||||
return true
|
||||
case reflect.Array:
|
||||
return containsPointers(typ.Elem())
|
||||
case reflect.Interface, reflect.Func:
|
||||
return true // err on the safe side.
|
||||
case reflect.Struct:
|
||||
if isWellKnownImmutableStruct(typ) {
|
||||
return false
|
||||
}
|
||||
for i := range typ.NumField() {
|
||||
if containsPointers(typ.Field(i).Type) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isWellKnownImmutableStruct(typ reflect.Type) bool {
|
||||
switch typ.String() {
|
||||
case "time.Time":
|
||||
// time.Time contains a pointer that does not need copying
|
||||
return true
|
||||
case "netip.Addr", "netip.Prefix", "netip.AddrPort":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
@@ -10,6 +10,7 @@ import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"unsafe"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
@@ -22,6 +23,16 @@ type viewStruct struct {
|
||||
StringsPtr *Slice[string] `json:",omitempty"`
|
||||
}
|
||||
|
||||
type noPtrStruct struct {
|
||||
Int int
|
||||
Str string
|
||||
}
|
||||
|
||||
type withPtrStruct struct {
|
||||
Int int
|
||||
StrPtr *string
|
||||
}
|
||||
|
||||
func BenchmarkSliceIteration(b *testing.B) {
|
||||
var data []viewStruct
|
||||
for i := range 10000 {
|
||||
@@ -189,3 +200,215 @@ func TestSliceMapKey(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestContainsPointers(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
typ reflect.Type
|
||||
wantPtrs bool
|
||||
}{
|
||||
{
|
||||
name: "bool",
|
||||
typ: reflect.TypeFor[bool](),
|
||||
wantPtrs: false,
|
||||
},
|
||||
{
|
||||
name: "int",
|
||||
typ: reflect.TypeFor[int](),
|
||||
wantPtrs: false,
|
||||
},
|
||||
{
|
||||
name: "int8",
|
||||
typ: reflect.TypeFor[int8](),
|
||||
wantPtrs: false,
|
||||
},
|
||||
{
|
||||
name: "int16",
|
||||
typ: reflect.TypeFor[int16](),
|
||||
wantPtrs: false,
|
||||
},
|
||||
{
|
||||
name: "int32",
|
||||
typ: reflect.TypeFor[int32](),
|
||||
wantPtrs: false,
|
||||
},
|
||||
{
|
||||
name: "int64",
|
||||
typ: reflect.TypeFor[int64](),
|
||||
wantPtrs: false,
|
||||
},
|
||||
{
|
||||
name: "uint",
|
||||
typ: reflect.TypeFor[uint](),
|
||||
wantPtrs: false,
|
||||
},
|
||||
{
|
||||
name: "uint8",
|
||||
typ: reflect.TypeFor[uint8](),
|
||||
wantPtrs: false,
|
||||
},
|
||||
{
|
||||
name: "uint16",
|
||||
typ: reflect.TypeFor[uint16](),
|
||||
wantPtrs: false,
|
||||
},
|
||||
{
|
||||
name: "uint32",
|
||||
typ: reflect.TypeFor[uint32](),
|
||||
wantPtrs: false,
|
||||
},
|
||||
{
|
||||
name: "uint64",
|
||||
typ: reflect.TypeFor[uint64](),
|
||||
wantPtrs: false,
|
||||
},
|
||||
{
|
||||
name: "uintptr",
|
||||
typ: reflect.TypeFor[uintptr](),
|
||||
wantPtrs: false,
|
||||
},
|
||||
{
|
||||
name: "string",
|
||||
typ: reflect.TypeFor[string](),
|
||||
wantPtrs: false,
|
||||
},
|
||||
{
|
||||
name: "float32",
|
||||
typ: reflect.TypeFor[float32](),
|
||||
wantPtrs: false,
|
||||
},
|
||||
{
|
||||
name: "float64",
|
||||
typ: reflect.TypeFor[float64](),
|
||||
wantPtrs: false,
|
||||
},
|
||||
{
|
||||
name: "complex64",
|
||||
typ: reflect.TypeFor[complex64](),
|
||||
wantPtrs: false,
|
||||
},
|
||||
{
|
||||
name: "complex128",
|
||||
typ: reflect.TypeFor[complex128](),
|
||||
wantPtrs: false,
|
||||
},
|
||||
{
|
||||
name: "netip-Addr",
|
||||
typ: reflect.TypeFor[netip.Addr](),
|
||||
wantPtrs: false,
|
||||
},
|
||||
{
|
||||
name: "netip-Prefix",
|
||||
typ: reflect.TypeFor[netip.Prefix](),
|
||||
wantPtrs: false,
|
||||
},
|
||||
{
|
||||
name: "netip-AddrPort",
|
||||
typ: reflect.TypeFor[netip.AddrPort](),
|
||||
wantPtrs: false,
|
||||
},
|
||||
{
|
||||
name: "bool-ptr",
|
||||
typ: reflect.TypeFor[*bool](),
|
||||
wantPtrs: true,
|
||||
},
|
||||
{
|
||||
name: "string-ptr",
|
||||
typ: reflect.TypeFor[*string](),
|
||||
wantPtrs: true,
|
||||
},
|
||||
{
|
||||
name: "netip-Addr-ptr",
|
||||
typ: reflect.TypeFor[*netip.Addr](),
|
||||
wantPtrs: true,
|
||||
},
|
||||
{
|
||||
name: "unsafe-ptr",
|
||||
typ: reflect.TypeFor[unsafe.Pointer](),
|
||||
wantPtrs: true,
|
||||
},
|
||||
{
|
||||
name: "no-ptr-struct",
|
||||
typ: reflect.TypeFor[noPtrStruct](),
|
||||
wantPtrs: false,
|
||||
},
|
||||
{
|
||||
name: "ptr-struct",
|
||||
typ: reflect.TypeFor[withPtrStruct](),
|
||||
wantPtrs: true,
|
||||
},
|
||||
{
|
||||
name: "string-array",
|
||||
typ: reflect.TypeFor[[5]string](),
|
||||
wantPtrs: false,
|
||||
},
|
||||
{
|
||||
name: "int-ptr-array",
|
||||
typ: reflect.TypeFor[[5]*int](),
|
||||
wantPtrs: true,
|
||||
},
|
||||
{
|
||||
name: "no-ptr-struct-array",
|
||||
typ: reflect.TypeFor[[5]noPtrStruct](),
|
||||
wantPtrs: false,
|
||||
},
|
||||
{
|
||||
name: "with-ptr-struct-array",
|
||||
typ: reflect.TypeFor[[5]withPtrStruct](),
|
||||
wantPtrs: true,
|
||||
},
|
||||
{
|
||||
name: "string-slice",
|
||||
typ: reflect.TypeFor[[]string](),
|
||||
wantPtrs: true,
|
||||
},
|
||||
{
|
||||
name: "int-ptr-slice",
|
||||
typ: reflect.TypeFor[[]int](),
|
||||
wantPtrs: true,
|
||||
},
|
||||
{
|
||||
name: "no-ptr-struct-slice",
|
||||
typ: reflect.TypeFor[[]noPtrStruct](),
|
||||
wantPtrs: true,
|
||||
},
|
||||
{
|
||||
name: "string-map",
|
||||
typ: reflect.TypeFor[map[string]string](),
|
||||
wantPtrs: true,
|
||||
},
|
||||
{
|
||||
name: "int-map",
|
||||
typ: reflect.TypeFor[map[int]int](),
|
||||
wantPtrs: true,
|
||||
},
|
||||
{
|
||||
name: "no-ptr-struct-map",
|
||||
typ: reflect.TypeFor[map[string]noPtrStruct](),
|
||||
wantPtrs: true,
|
||||
},
|
||||
{
|
||||
name: "chan",
|
||||
typ: reflect.TypeFor[chan int](),
|
||||
wantPtrs: true,
|
||||
},
|
||||
{
|
||||
name: "func",
|
||||
typ: reflect.TypeFor[func()](),
|
||||
wantPtrs: true,
|
||||
},
|
||||
{
|
||||
name: "interface",
|
||||
typ: reflect.TypeFor[any](),
|
||||
wantPtrs: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if gotPtrs := containsPointers(tt.typ); gotPtrs != tt.wantPtrs {
|
||||
t.Errorf("got %v; want %v", gotPtrs, tt.wantPtrs)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user