mirror of
				https://github.com/tailscale/tailscale.git
				synced 2025-10-31 00:01:40 +01:00 
			
		
		
		
	Extract field comments from AST and include them in generated view methods. Comments are preserved from the original struct fields to provide documentation for the view accessors. Fixes #16958 Signed-off-by: Maisem Ali <3953239+maisem@users.noreply.github.com>
		
			
				
	
	
		
			761 lines
		
	
	
		
			25 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			761 lines
		
	
	
		
			25 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright (c) Tailscale Inc & AUTHORS
 | |
| // SPDX-License-Identifier: BSD-3-Clause
 | |
| 
 | |
| // Viewer is a tool to automate the creation of "view" wrapper types that
 | |
| // provide read-only accessor methods to underlying fields.
 | |
| package main
 | |
| 
 | |
| import (
 | |
| 	"bytes"
 | |
| 	"flag"
 | |
| 	"fmt"
 | |
| 	"go/ast"
 | |
| 	"go/token"
 | |
| 	"go/types"
 | |
| 	"html/template"
 | |
| 	"log"
 | |
| 	"os"
 | |
| 	"slices"
 | |
| 	"strings"
 | |
| 
 | |
| 	"tailscale.com/util/codegen"
 | |
| 	"tailscale.com/util/mak"
 | |
| 	"tailscale.com/util/must"
 | |
| )
 | |
| 
 | |
| const viewTemplateStr = `{{define "common"}}
 | |
| // View returns a read-only view of {{.StructName}}.
 | |
| func (p *{{.StructName}}{{.TypeParamNames}}) View() {{.ViewName}}{{.TypeParamNames}} {
 | |
| 	return {{.ViewName}}{{.TypeParamNames}}{ж: p}
 | |
| }
 | |
| 
 | |
| // {{.ViewName}}{{.TypeParamNames}} provides a read-only view over {{.StructName}}{{.TypeParamNames}}.
 | |
| //
 | |
| // Its methods should only be called if ` + "`Valid()`" + ` returns true.
 | |
| type {{.ViewName}}{{.TypeParams}} struct {
 | |
| 	// ж is the underlying mutable value, named with a hard-to-type
 | |
| 	// character that looks pointy like a pointer.
 | |
| 	// It is named distinctively to make you think of how dangerous it is to escape
 | |
| 	// to callers. You must not let callers be able to mutate it.
 | |
| 	ж *{{.StructName}}{{.TypeParamNames}}
 | |
| }
 | |
| 
 | |
| // Valid reports whether v's underlying value is non-nil.
 | |
| func (v {{.ViewName}}{{.TypeParamNames}}) Valid() bool { return v.ж != nil }
 | |
| 
 | |
| // AsStruct returns a clone of the underlying value which aliases no memory with
 | |
| // the original.
 | |
| func (v {{.ViewName}}{{.TypeParamNames}}) AsStruct() *{{.StructName}}{{.TypeParamNames}}{ 
 | |
| 	if v.ж == nil {
 | |
| 		return nil
 | |
| 	}
 | |
| 	return v.ж.Clone()
 | |
| }
 | |
| 
 | |
| // MarshalJSON implements [jsonv1.Marshaler].
 | |
| func (v {{.ViewName}}{{.TypeParamNames}}) MarshalJSON() ([]byte, error) {
 | |
| 	return jsonv1.Marshal(v.ж)
 | |
| }
 | |
| 
 | |
| // MarshalJSONTo implements [jsonv2.MarshalerTo].
 | |
| func (v {{.ViewName}}{{.TypeParamNames}}) MarshalJSONTo(enc *jsontext.Encoder) error {
 | |
| 	return jsonv2.MarshalEncode(enc, v.ж)
 | |
| }
 | |
| 
 | |
| // UnmarshalJSON implements [jsonv1.Unmarshaler].
 | |
| func (v *{{.ViewName}}{{.TypeParamNames}}) UnmarshalJSON(b []byte) error {
 | |
| 	if v.ж != nil {
 | |
| 		return errors.New("already initialized")
 | |
| 	}
 | |
| 	if len(b) == 0 {
 | |
| 		return nil
 | |
| 	}
 | |
| 	var x {{.StructName}}{{.TypeParamNames}}
 | |
| 	if err := jsonv1.Unmarshal(b, &x); err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	v.ж = &x
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
 | |
| func (v *{{.ViewName}}{{.TypeParamNames}}) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
 | |
| 	if v.ж != nil {
 | |
| 		return errors.New("already initialized")
 | |
| 	}
 | |
| 	var x {{.StructName}}{{.TypeParamNames}}
 | |
| 	if err := jsonv2.UnmarshalDecode(dec, &x); err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	v.ж = &x
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| {{end}}
 | |
| {{define "valueField"}}func (v {{.ViewName}}{{.TypeParamNames}}) {{.FieldName}}() {{.FieldType}} { return v.ж.{{.FieldName}} }
 | |
| {{end}}
 | |
| {{define "byteSliceField"}}func (v {{.ViewName}}{{.TypeParamNames}}) {{.FieldName}}() views.ByteSlice[{{.FieldType}}] { return views.ByteSliceOf(v.ж.{{.FieldName}}) }
 | |
| {{end}}
 | |
| {{define "sliceField"}}func (v {{.ViewName}}{{.TypeParamNames}}) {{.FieldName}}() views.Slice[{{.FieldType}}] { return views.SliceOf(v.ж.{{.FieldName}}) }
 | |
| {{end}}
 | |
| {{define "viewSliceField"}}func (v {{.ViewName}}{{.TypeParamNames}}) {{.FieldName}}() views.SliceView[{{.FieldType}},{{.FieldViewName}}] { return views.SliceOfViews[{{.FieldType}},{{.FieldViewName}}](v.ж.{{.FieldName}}) }
 | |
| {{end}}
 | |
| {{define "viewField"}}func (v {{.ViewName}}{{.TypeParamNames}}) {{.FieldName}}() {{.FieldViewName}} { return v.ж.{{.FieldName}}.View() }
 | |
| {{end}}
 | |
| {{define "makeViewField"}}func (v {{.ViewName}}{{.TypeParamNames}}) {{.FieldName}}() {{.FieldViewName}} { return {{.MakeViewFnName}}(&v.ж.{{.FieldName}}) }
 | |
| {{end}}
 | |
| {{define "valuePointerField"}}func (v {{.ViewName}}{{.TypeParamNames}}) {{.FieldName}}() views.ValuePointer[{{.FieldType}}] { return views.ValuePointerOf(v.ж.{{.FieldName}}) }
 | |
| 
 | |
| {{end}}
 | |
| {{define "mapField"}}func(v {{.ViewName}}{{.TypeParamNames}}) {{.FieldName}}() views.Map[{{.MapKeyType}},{{.MapValueType}}] { return views.MapOf(v.ж.{{.FieldName}})}
 | |
| {{end}}
 | |
| {{define "mapFnField"}}func(v {{.ViewName}}{{.TypeParamNames}}) {{.FieldName}}() views.MapFn[{{.MapKeyType}},{{.MapValueType}},{{.MapValueView}}] { return views.MapFnOf(v.ж.{{.FieldName}}, func (t {{.MapValueType}}) {{.MapValueView}} {
 | |
| 	return {{.MapFn}}
 | |
| })}
 | |
| {{end}}
 | |
| {{define "mapSliceField"}}func(v {{.ViewName}}{{.TypeParamNames}}) {{.FieldName}}() views.MapSlice[{{.MapKeyType}},{{.MapValueType}}] { return views.MapSliceOf(v.ж.{{.FieldName}}) }
 | |
| {{end}}
 | |
| {{define "unsupportedField"}}func(v {{.ViewName}}{{.TypeParamNames}}) {{.FieldName}}() {{.FieldType}} {panic("unsupported")}
 | |
| {{end}}
 | |
| {{define "stringFunc"}}func(v {{.ViewName}}{{.TypeParamNames}}) String() string { return v.ж.String() }
 | |
| {{end}}
 | |
| {{define "equalFunc"}}func(v {{.ViewName}}{{.TypeParamNames}}) Equal(v2 {{.ViewName}}{{.TypeParamNames}}) bool { return v.ж.Equal(v2.ж) }
 | |
| {{end}}
 | |
| `
 | |
| 
 | |
| var viewTemplate *template.Template
 | |
| 
 | |
| func init() {
 | |
| 	viewTemplate = template.Must(template.New("view").Parse(viewTemplateStr))
 | |
| }
 | |
| 
 | |
| func requiresCloning(t types.Type) (shallow, deep bool, base types.Type) {
 | |
| 	switch v := t.(type) {
 | |
| 	case *types.Pointer:
 | |
| 		_, deep, base = requiresCloning(v.Elem())
 | |
| 		return true, deep, base
 | |
| 	case *types.Slice:
 | |
| 		_, deep, base = requiresCloning(v.Elem())
 | |
| 		return true, deep, base
 | |
| 	}
 | |
| 	p := codegen.ContainsPointers(t)
 | |
| 	return p, p, t
 | |
| }
 | |
| 
 | |
| type fieldNameKey struct {
 | |
| 	typeName  string
 | |
| 	fieldName string
 | |
| }
 | |
| 
 | |
| // getFieldComments extracts field comments from the AST for a given struct type.
 | |
| func getFieldComments(syntax []*ast.File) map[fieldNameKey]string {
 | |
| 	if len(syntax) == 0 {
 | |
| 		return nil
 | |
| 	}
 | |
| 	var fieldComments map[fieldNameKey]string
 | |
| 
 | |
| 	// Search through all AST files in the package
 | |
| 	for _, file := range syntax {
 | |
| 		// Look for the type declaration
 | |
| 		for _, decl := range file.Decls {
 | |
| 			genDecl, ok := decl.(*ast.GenDecl)
 | |
| 			if !ok || genDecl.Tok != token.TYPE {
 | |
| 				continue
 | |
| 			}
 | |
| 
 | |
| 			for _, spec := range genDecl.Specs {
 | |
| 				typeSpec, ok := spec.(*ast.TypeSpec)
 | |
| 				if !ok {
 | |
| 					continue
 | |
| 				}
 | |
| 				typeName := typeSpec.Name.Name
 | |
| 
 | |
| 				// Check if it's a struct type
 | |
| 				structType, ok := typeSpec.Type.(*ast.StructType)
 | |
| 				if !ok {
 | |
| 					continue
 | |
| 				}
 | |
| 
 | |
| 				// Extract field comments
 | |
| 				for _, field := range structType.Fields.List {
 | |
| 					if len(field.Names) == 0 {
 | |
| 						// Anonymous field or no names
 | |
| 						continue
 | |
| 					}
 | |
| 
 | |
| 					// Get the field name
 | |
| 					fieldName := field.Names[0].Name
 | |
| 					key := fieldNameKey{typeName, fieldName}
 | |
| 
 | |
| 					// Get the comment
 | |
| 					var comment string
 | |
| 					if field.Doc != nil && field.Doc.Text() != "" {
 | |
| 						// Format the comment for Go code generation
 | |
| 						comment = strings.TrimSpace(field.Doc.Text())
 | |
| 						// Convert multi-line comments to proper Go comment format
 | |
| 						var sb strings.Builder
 | |
| 						for line := range strings.Lines(comment) {
 | |
| 							sb.WriteString("// ")
 | |
| 							sb.WriteString(line)
 | |
| 						}
 | |
| 						if sb.Len() > 0 {
 | |
| 							comment = sb.String()
 | |
| 						}
 | |
| 					} else if field.Comment != nil && field.Comment.Text() != "" {
 | |
| 						// Handle inline comments
 | |
| 						comment = "// " + strings.TrimSpace(field.Comment.Text())
 | |
| 					}
 | |
| 					if comment != "" {
 | |
| 						mak.Set(&fieldComments, key, comment)
 | |
| 					}
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return fieldComments
 | |
| }
 | |
| 
 | |
| func genView(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named, fieldComments map[fieldNameKey]string) {
 | |
| 	t, ok := typ.Underlying().(*types.Struct)
 | |
| 	if !ok || codegen.IsViewType(t) {
 | |
| 		return
 | |
| 	}
 | |
| 	it.Import("jsonv1", "encoding/json")
 | |
| 	it.Import("jsonv2", "github.com/go-json-experiment/json")
 | |
| 	it.Import("", "github.com/go-json-experiment/json/jsontext")
 | |
| 	it.Import("", "errors")
 | |
| 
 | |
| 	args := struct {
 | |
| 		StructName     string
 | |
| 		ViewName       string
 | |
| 		TypeParams     string // e.g. [T constraints.Integer]
 | |
| 		TypeParamNames string // e.g. [T]
 | |
| 
 | |
| 		FieldName     string
 | |
| 		FieldType     string
 | |
| 		FieldViewName string
 | |
| 
 | |
| 		MapKeyType   string
 | |
| 		MapValueType string
 | |
| 		MapValueView string
 | |
| 		MapFn        string
 | |
| 
 | |
| 		// MakeViewFnName is the name of the function that accepts a value and returns a read-only view of it.
 | |
| 		MakeViewFnName string
 | |
| 	}{
 | |
| 		StructName: typ.Obj().Name(),
 | |
| 		ViewName:   typ.Origin().Obj().Name() + "View",
 | |
| 	}
 | |
| 
 | |
| 	typeParams := typ.Origin().TypeParams()
 | |
| 	args.TypeParams, args.TypeParamNames = codegen.FormatTypeParams(typeParams, it)
 | |
| 
 | |
| 	writeTemplate := func(name string) {
 | |
| 		if err := viewTemplate.ExecuteTemplate(buf, name, args); err != nil {
 | |
| 			log.Fatal(err)
 | |
| 		}
 | |
| 	}
 | |
| 	writeTemplateWithComment := func(name, fieldName string) {
 | |
| 		// Write the field comment if it exists
 | |
| 		key := fieldNameKey{args.StructName, fieldName}
 | |
| 		if comment, ok := fieldComments[key]; ok && comment != "" {
 | |
| 			fmt.Fprintln(buf, comment)
 | |
| 		}
 | |
| 		writeTemplate(name)
 | |
| 	}
 | |
| 
 | |
| 	writeTemplate("common")
 | |
| 	for i := range t.NumFields() {
 | |
| 		f := t.Field(i)
 | |
| 		fname := f.Name()
 | |
| 		if !f.Exported() {
 | |
| 			continue
 | |
| 		}
 | |
| 		args.FieldName = fname
 | |
| 		fieldType := f.Type()
 | |
| 		if codegen.IsInvalid(fieldType) {
 | |
| 			continue
 | |
| 		}
 | |
| 		if !codegen.ContainsPointers(fieldType) || codegen.IsViewType(fieldType) || codegen.HasNoClone(t.Tag(i)) {
 | |
| 			args.FieldType = it.QualifiedName(fieldType)
 | |
| 			writeTemplateWithComment("valueField", fname)
 | |
| 			continue
 | |
| 		}
 | |
| 		switch underlying := fieldType.Underlying().(type) {
 | |
| 		case *types.Slice:
 | |
| 			slice := underlying
 | |
| 			elem := slice.Elem()
 | |
| 			switch elem.String() {
 | |
| 			case "byte":
 | |
| 				args.FieldType = it.QualifiedName(fieldType)
 | |
| 				it.Import("", "tailscale.com/types/views")
 | |
| 				writeTemplateWithComment("byteSliceField", fname)
 | |
| 			default:
 | |
| 				args.FieldType = it.QualifiedName(elem)
 | |
| 				it.Import("", "tailscale.com/types/views")
 | |
| 				shallow, deep, base := requiresCloning(elem)
 | |
| 				if deep {
 | |
| 					switch elem.Underlying().(type) {
 | |
| 					case *types.Pointer:
 | |
| 						if _, isIface := base.Underlying().(*types.Interface); !isIface {
 | |
| 							args.FieldViewName = appendNameSuffix(it.QualifiedName(base), "View")
 | |
| 							writeTemplateWithComment("viewSliceField", fname)
 | |
| 						} else {
 | |
| 							writeTemplateWithComment("unsupportedField", fname)
 | |
| 						}
 | |
| 						continue
 | |
| 					case *types.Interface:
 | |
| 						if viewType := viewTypeForValueType(elem); viewType != nil {
 | |
| 							args.FieldViewName = it.QualifiedName(viewType)
 | |
| 							writeTemplateWithComment("viewSliceField", fname)
 | |
| 							continue
 | |
| 						}
 | |
| 					}
 | |
| 					writeTemplateWithComment("unsupportedField", fname)
 | |
| 					continue
 | |
| 				} else if shallow {
 | |
| 					switch base.Underlying().(type) {
 | |
| 					case *types.Basic, *types.Interface:
 | |
| 						writeTemplateWithComment("unsupportedField", fname)
 | |
| 					default:
 | |
| 						if _, isIface := base.Underlying().(*types.Interface); !isIface {
 | |
| 							args.FieldViewName = appendNameSuffix(it.QualifiedName(base), "View")
 | |
| 							writeTemplateWithComment("viewSliceField", fname)
 | |
| 						} else {
 | |
| 							writeTemplateWithComment("unsupportedField", fname)
 | |
| 						}
 | |
| 					}
 | |
| 					continue
 | |
| 				}
 | |
| 				writeTemplateWithComment("sliceField", fname)
 | |
| 			}
 | |
| 			continue
 | |
| 		case *types.Struct:
 | |
| 			strucT := underlying
 | |
| 			args.FieldType = it.QualifiedName(fieldType)
 | |
| 			if codegen.ContainsPointers(strucT) {
 | |
| 				if viewType := viewTypeForValueType(fieldType); viewType != nil {
 | |
| 					args.FieldViewName = it.QualifiedName(viewType)
 | |
| 					writeTemplateWithComment("viewField", fname)
 | |
| 					continue
 | |
| 				}
 | |
| 				if viewType, makeViewFn := viewTypeForContainerType(fieldType); viewType != nil {
 | |
| 					args.FieldViewName = it.QualifiedName(viewType)
 | |
| 					args.MakeViewFnName = it.PackagePrefix(makeViewFn.Pkg()) + makeViewFn.Name()
 | |
| 					writeTemplateWithComment("makeViewField", fname)
 | |
| 					continue
 | |
| 				}
 | |
| 				writeTemplateWithComment("unsupportedField", fname)
 | |
| 				continue
 | |
| 			}
 | |
| 			writeTemplateWithComment("valueField", fname)
 | |
| 			continue
 | |
| 		case *types.Map:
 | |
| 			m := underlying
 | |
| 			args.FieldType = it.QualifiedName(fieldType)
 | |
| 			shallow, deep, key := requiresCloning(m.Key())
 | |
| 			if shallow || deep {
 | |
| 				writeTemplateWithComment("unsupportedField", fname)
 | |
| 				continue
 | |
| 			}
 | |
| 			it.Import("", "tailscale.com/types/views")
 | |
| 			args.MapKeyType = it.QualifiedName(key)
 | |
| 			mElem := m.Elem()
 | |
| 			var template string
 | |
| 			switch u := mElem.(type) {
 | |
| 			case *types.Struct, *types.Named, *types.Alias:
 | |
| 				strucT := u
 | |
| 				args.FieldType = it.QualifiedName(fieldType)
 | |
| 				if codegen.ContainsPointers(strucT) {
 | |
| 					args.MapFn = "t.View()"
 | |
| 					template = "mapFnField"
 | |
| 					args.MapValueType = it.QualifiedName(mElem)
 | |
| 					args.MapValueView = appendNameSuffix(args.MapValueType, "View")
 | |
| 				} else {
 | |
| 					template = "mapField"
 | |
| 					args.MapValueType = it.QualifiedName(mElem)
 | |
| 				}
 | |
| 			case *types.Basic:
 | |
| 				template = "mapField"
 | |
| 				args.MapValueType = it.QualifiedName(mElem)
 | |
| 			case *types.Slice:
 | |
| 				slice := u
 | |
| 				sElem := slice.Elem()
 | |
| 				switch x := sElem.(type) {
 | |
| 				case *types.Basic, *types.Named, *types.Alias:
 | |
| 					sElem := it.QualifiedName(sElem)
 | |
| 					args.MapValueView = fmt.Sprintf("views.Slice[%v]", sElem)
 | |
| 					args.MapValueType = sElem
 | |
| 					template = "mapSliceField"
 | |
| 				case *types.Pointer:
 | |
| 					ptr := x
 | |
| 					pElem := ptr.Elem()
 | |
| 					template = "unsupportedField"
 | |
| 					if _, isIface := pElem.Underlying().(*types.Interface); !isIface {
 | |
| 						switch pElem.(type) {
 | |
| 						case *types.Struct, *types.Named, *types.Alias:
 | |
| 							ptrType := it.QualifiedName(ptr)
 | |
| 							viewType := appendNameSuffix(it.QualifiedName(pElem), "View")
 | |
| 							args.MapFn = fmt.Sprintf("views.SliceOfViews[%v,%v](t)", ptrType, viewType)
 | |
| 							args.MapValueView = fmt.Sprintf("views.SliceView[%v,%v]", ptrType, viewType)
 | |
| 							args.MapValueType = "[]" + ptrType
 | |
| 							template = "mapFnField"
 | |
| 						default:
 | |
| 							template = "unsupportedField"
 | |
| 						}
 | |
| 					} else {
 | |
| 						template = "unsupportedField"
 | |
| 					}
 | |
| 				default:
 | |
| 					template = "unsupportedField"
 | |
| 				}
 | |
| 			case *types.Pointer:
 | |
| 				ptr := u
 | |
| 				pElem := ptr.Elem()
 | |
| 				if _, isIface := pElem.Underlying().(*types.Interface); !isIface {
 | |
| 					switch pElem.(type) {
 | |
| 					case *types.Struct, *types.Named, *types.Alias:
 | |
| 						args.MapValueType = it.QualifiedName(ptr)
 | |
| 						args.MapValueView = appendNameSuffix(it.QualifiedName(pElem), "View")
 | |
| 						args.MapFn = "t.View()"
 | |
| 						template = "mapFnField"
 | |
| 					default:
 | |
| 						template = "unsupportedField"
 | |
| 					}
 | |
| 				} else {
 | |
| 					template = "unsupportedField"
 | |
| 				}
 | |
| 			case *types.Interface, *types.TypeParam:
 | |
| 				if viewType := viewTypeForValueType(u); viewType != nil {
 | |
| 					args.MapValueType = it.QualifiedName(u)
 | |
| 					args.MapValueView = it.QualifiedName(viewType)
 | |
| 					args.MapFn = "t.View()"
 | |
| 					template = "mapFnField"
 | |
| 				} else if !codegen.ContainsPointers(u) {
 | |
| 					args.MapValueType = it.QualifiedName(mElem)
 | |
| 					template = "mapField"
 | |
| 				} else {
 | |
| 					template = "unsupportedField"
 | |
| 				}
 | |
| 			default:
 | |
| 				template = "unsupportedField"
 | |
| 			}
 | |
| 			writeTemplateWithComment(template, fname)
 | |
| 			continue
 | |
| 		case *types.Pointer:
 | |
| 			ptr := underlying
 | |
| 			_, deep, base := requiresCloning(ptr)
 | |
| 
 | |
| 			if deep {
 | |
| 				if _, isIface := base.Underlying().(*types.Interface); !isIface {
 | |
| 					args.FieldType = it.QualifiedName(base)
 | |
| 					args.FieldViewName = appendNameSuffix(args.FieldType, "View")
 | |
| 					writeTemplateWithComment("viewField", fname)
 | |
| 				} else {
 | |
| 					writeTemplateWithComment("unsupportedField", fname)
 | |
| 				}
 | |
| 				continue
 | |
| 			}
 | |
| 
 | |
| 			// If a view type is already defined for the base type, use it as the field's view type.
 | |
| 			if viewType := viewTypeForValueType(base); viewType != nil {
 | |
| 				args.FieldType = it.QualifiedName(base)
 | |
| 				args.FieldViewName = it.QualifiedName(viewType)
 | |
| 				writeTemplateWithComment("viewField", fname)
 | |
| 				continue
 | |
| 			}
 | |
| 
 | |
| 			// Otherwise, if the unaliased base type is a named type whose view type will be generated by this viewer invocation,
 | |
| 			// append the "View" suffix to the unaliased base type name and use it as the field's view type.
 | |
| 			if base, ok := types.Unalias(base).(*types.Named); ok && slices.Contains(typeNames, it.QualifiedName(base)) {
 | |
| 				baseTypeName := it.QualifiedName(base)
 | |
| 				args.FieldType = baseTypeName
 | |
| 				args.FieldViewName = appendNameSuffix(args.FieldType, "View")
 | |
| 				writeTemplateWithComment("viewField", fname)
 | |
| 				continue
 | |
| 			}
 | |
| 
 | |
| 			// Otherwise, if the base type does not require deep cloning, has no existing view type,
 | |
| 			// and will not have a generated view type, use views.ValuePointer[T] as the field's view type.
 | |
| 			// Its Get/GetOk methods return stack-allocated shallow copies of the field's value.
 | |
| 			args.FieldType = it.QualifiedName(base)
 | |
| 			writeTemplateWithComment("valuePointerField", fname)
 | |
| 			continue
 | |
| 		case *types.Interface:
 | |
| 			// If fieldType is an interface with a "View() {ViewType}" method, it can be used to clone the field.
 | |
| 			// This includes scenarios where fieldType is a constrained type parameter.
 | |
| 			if viewType := viewTypeForValueType(underlying); viewType != nil {
 | |
| 				args.FieldViewName = it.QualifiedName(viewType)
 | |
| 				writeTemplateWithComment("viewField", fname)
 | |
| 				continue
 | |
| 			}
 | |
| 		}
 | |
| 		writeTemplateWithComment("unsupportedField", fname)
 | |
| 	}
 | |
| 	for i := range typ.NumMethods() {
 | |
| 		f := typ.Method(i)
 | |
| 		if !f.Exported() {
 | |
| 			continue
 | |
| 		}
 | |
| 		sig, ok := f.Type().(*types.Signature)
 | |
| 		if !ok {
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		switch f.Name() {
 | |
| 		case "Clone", "View":
 | |
| 			continue // "AsStruct"
 | |
| 		case "String":
 | |
| 			writeTemplate("stringFunc")
 | |
| 			continue
 | |
| 		case "Equal":
 | |
| 			if sig.Results().Len() == 1 && sig.Results().At(0).Type().String() == "bool" {
 | |
| 				writeTemplate("equalFunc")
 | |
| 				continue
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	fmt.Fprintf(buf, "\n")
 | |
| 	buf.Write(codegen.AssertStructUnchanged(t, args.StructName, typeParams, "View", it))
 | |
| }
 | |
| 
 | |
| func appendNameSuffix(name, suffix string) string {
 | |
| 	if idx := strings.IndexRune(name, '['); idx != -1 {
 | |
| 		// Insert suffix after the type name, but before type parameters.
 | |
| 		return name[:idx] + suffix + name[idx:]
 | |
| 	}
 | |
| 	return name + suffix
 | |
| }
 | |
| 
 | |
| func typeNameOf(typ types.Type) (name *types.TypeName, ok bool) {
 | |
| 	switch t := typ.(type) {
 | |
| 	case *types.Alias:
 | |
| 		return t.Obj(), true
 | |
| 	case *types.Named:
 | |
| 		return t.Obj(), true
 | |
| 	default:
 | |
| 		return nil, false
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func lookupViewType(typ types.Type) types.Type {
 | |
| 	for {
 | |
| 		if typeName, ok := typeNameOf(typ); ok && typeName.Pkg() != nil {
 | |
| 			if viewTypeObj := typeName.Pkg().Scope().Lookup(typeName.Name() + "View"); viewTypeObj != nil {
 | |
| 				return viewTypeObj.Type()
 | |
| 			}
 | |
| 		}
 | |
| 		switch alias := typ.(type) {
 | |
| 		case *types.Alias:
 | |
| 			typ = alias.Rhs()
 | |
| 		default:
 | |
| 			return nil
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func viewTypeForValueType(typ types.Type) types.Type {
 | |
| 	if ptr, ok := typ.(*types.Pointer); ok {
 | |
| 		return viewTypeForValueType(ptr.Elem())
 | |
| 	}
 | |
| 	viewMethod := codegen.LookupMethod(typ, "View")
 | |
| 	if viewMethod == nil {
 | |
| 		return nil
 | |
| 	}
 | |
| 	sig, ok := viewMethod.Type().(*types.Signature)
 | |
| 	if !ok || sig.Results().Len() != 1 {
 | |
| 		return nil
 | |
| 	}
 | |
| 	viewType := sig.Results().At(0).Type()
 | |
| 	// Check if the typ's package defines an alias for the view type, and use it if so.
 | |
| 	if viewTypeAlias, ok := lookupViewType(typ).(*types.Alias); ok && types.AssignableTo(viewType, viewTypeAlias) {
 | |
| 		viewType = viewTypeAlias
 | |
| 	}
 | |
| 	return viewType
 | |
| }
 | |
| 
 | |
| func viewTypeForContainerType(typ types.Type) (*types.Named, *types.Func) {
 | |
| 	// The container type should be an instantiated generic type,
 | |
| 	// with its first type parameter specifying the element type.
 | |
| 	containerType, ok := codegen.NamedTypeOf(typ)
 | |
| 	if !ok || containerType.TypeArgs().Len() == 0 {
 | |
| 		return nil, nil
 | |
| 	}
 | |
| 
 | |
| 	// Look up the view type for the container type.
 | |
| 	// It must include an additional type parameter specifying the element's view type.
 | |
| 	// For example, Container[T] => ContainerView[T, V].
 | |
| 	containerViewTypeName := containerType.Obj().Name() + "View"
 | |
| 	containerViewTypeObj, ok := containerType.Obj().Pkg().Scope().Lookup(containerViewTypeName).(*types.TypeName)
 | |
| 	if !ok {
 | |
| 		return nil, nil
 | |
| 	}
 | |
| 	containerViewGenericType, ok := codegen.NamedTypeOf(containerViewTypeObj.Type())
 | |
| 	if !ok || containerViewGenericType.TypeParams().Len() != containerType.TypeArgs().Len()+1 {
 | |
| 		return nil, nil
 | |
| 	}
 | |
| 
 | |
| 	// Create a list of type arguments for instantiating the container view type.
 | |
| 	// Include all type arguments specified for the container type...
 | |
| 	containerViewTypeArgs := make([]types.Type, containerViewGenericType.TypeParams().Len())
 | |
| 	for i := range containerType.TypeArgs().Len() {
 | |
| 		containerViewTypeArgs[i] = containerType.TypeArgs().At(i)
 | |
| 	}
 | |
| 	// ...and add the element view type.
 | |
| 	// For that, we need to first determine the named elem type...
 | |
| 	elemType, ok := codegen.NamedTypeOf(baseType(containerType.TypeArgs().At(containerType.TypeArgs().Len() - 1)))
 | |
| 	if !ok {
 | |
| 		return nil, nil
 | |
| 	}
 | |
| 	// ...then infer the view type from it.
 | |
| 	var elemViewType *types.Named
 | |
| 	elemTypeName := elemType.Obj().Name()
 | |
| 	elemViewTypeBaseName := elemType.Obj().Name() + "View"
 | |
| 	if elemViewTypeName, ok := elemType.Obj().Pkg().Scope().Lookup(elemViewTypeBaseName).(*types.TypeName); ok {
 | |
| 		// The elem's view type is already defined in the same package as the elem type.
 | |
| 		elemViewType = elemViewTypeName.Type().(*types.Named)
 | |
| 	} else if slices.Contains(typeNames, elemTypeName) {
 | |
| 		// The elem's view type has not been generated yet, but we can define
 | |
| 		// and use a blank type with the expected view type name.
 | |
| 		elemViewTypeName = types.NewTypeName(0, elemType.Obj().Pkg(), elemViewTypeBaseName, nil)
 | |
| 		elemViewType = types.NewNamed(elemViewTypeName, types.NewStruct(nil, nil), nil)
 | |
| 		if elemTypeParams := elemType.TypeParams(); elemTypeParams != nil {
 | |
| 			elemViewType.SetTypeParams(collectTypeParams(elemTypeParams))
 | |
| 		}
 | |
| 	} else {
 | |
| 		// The elem view type does not exist and won't be generated.
 | |
| 		return nil, nil
 | |
| 	}
 | |
| 	// If elemType is an instantiated generic type, instantiate the elemViewType as well.
 | |
| 	if elemTypeArgs := elemType.TypeArgs(); elemTypeArgs != nil {
 | |
| 		elemViewType, _ = codegen.NamedTypeOf(must.Get(types.Instantiate(nil, elemViewType, collectTypes(elemTypeArgs), false)))
 | |
| 	}
 | |
| 	// And finally set the elemViewType as the last type argument.
 | |
| 	containerViewTypeArgs[len(containerViewTypeArgs)-1] = elemViewType
 | |
| 
 | |
| 	// Instantiate the container view type with the specified type arguments.
 | |
| 	containerViewType := must.Get(types.Instantiate(nil, containerViewGenericType, containerViewTypeArgs, false))
 | |
| 	// Look up a function to create a view of a container.
 | |
| 	// It should be in the same package as the container type, named {ViewType}Of,
 | |
| 	// and have a signature like {ViewType}Of(c *Container[T]) ContainerView[T, V].
 | |
| 	makeContainerView, ok := containerType.Obj().Pkg().Scope().Lookup(containerViewTypeName + "Of").(*types.Func)
 | |
| 	if !ok {
 | |
| 		return nil, nil
 | |
| 	}
 | |
| 	return containerViewType.(*types.Named), makeContainerView
 | |
| }
 | |
| 
 | |
| func baseType(typ types.Type) types.Type {
 | |
| 	if ptr, ok := typ.(*types.Pointer); ok {
 | |
| 		return ptr.Elem()
 | |
| 	}
 | |
| 	return typ
 | |
| }
 | |
| 
 | |
| func collectTypes(list *types.TypeList) []types.Type {
 | |
| 	// TODO(nickkhyl): use slices.Collect in Go 1.23?
 | |
| 	if list.Len() == 0 {
 | |
| 		return nil
 | |
| 	}
 | |
| 	res := make([]types.Type, list.Len())
 | |
| 	for i := range res {
 | |
| 		res[i] = list.At(i)
 | |
| 	}
 | |
| 	return res
 | |
| }
 | |
| 
 | |
| func collectTypeParams(list *types.TypeParamList) []*types.TypeParam {
 | |
| 	if list.Len() == 0 {
 | |
| 		return nil
 | |
| 	}
 | |
| 	res := make([]*types.TypeParam, list.Len())
 | |
| 	for i := range res {
 | |
| 		p := list.At(i)
 | |
| 		res[i] = types.NewTypeParam(p.Obj(), p.Constraint())
 | |
| 	}
 | |
| 	return res
 | |
| }
 | |
| 
 | |
| var (
 | |
| 	flagTypes     = flag.String("type", "", "comma-separated list of types; required")
 | |
| 	flagBuildTags = flag.String("tags", "", "compiler build tags to apply")
 | |
| 	flagCloneFunc = flag.Bool("clonefunc", false, "add a top-level Clone func")
 | |
| 
 | |
| 	flagCloneOnlyTypes = flag.String("clone-only-type", "", "comma-separated list of types (a subset of --type) that should only generate a go:generate clone line and not actual views")
 | |
| 
 | |
| 	typeNames []string
 | |
| )
 | |
| 
 | |
| func main() {
 | |
| 	log.SetFlags(0)
 | |
| 	log.SetPrefix("viewer: ")
 | |
| 	flag.Parse()
 | |
| 	if len(*flagTypes) == 0 {
 | |
| 		flag.Usage()
 | |
| 		os.Exit(2)
 | |
| 	}
 | |
| 	typeNames = strings.Split(*flagTypes, ",")
 | |
| 
 | |
| 	var flagArgs []string
 | |
| 	flagArgs = append(flagArgs, fmt.Sprintf("-clonefunc=%v", *flagCloneFunc))
 | |
| 	if *flagTypes != "" {
 | |
| 		flagArgs = append(flagArgs, "-type="+*flagTypes)
 | |
| 	}
 | |
| 	if *flagBuildTags != "" {
 | |
| 		flagArgs = append(flagArgs, "-tags="+*flagBuildTags)
 | |
| 	}
 | |
| 	pkg, namedTypes, err := codegen.LoadTypes(*flagBuildTags, ".")
 | |
| 	if err != nil {
 | |
| 		log.Fatal(err)
 | |
| 	}
 | |
| 	it := codegen.NewImportTracker(pkg.Types)
 | |
| 	fieldComments := getFieldComments(pkg.Syntax)
 | |
| 
 | |
| 	cloneOnlyType := map[string]bool{}
 | |
| 	for _, t := range strings.Split(*flagCloneOnlyTypes, ",") {
 | |
| 		cloneOnlyType[t] = true
 | |
| 	}
 | |
| 
 | |
| 	buf := new(bytes.Buffer)
 | |
| 	fmt.Fprintf(buf, "//go:generate go run tailscale.com/cmd/cloner  %s\n\n", strings.Join(flagArgs, " "))
 | |
| 	runCloner := false
 | |
| 	for _, typeName := range typeNames {
 | |
| 		if cloneOnlyType[typeName] {
 | |
| 			continue
 | |
| 		}
 | |
| 		typ, ok := namedTypes[typeName].(*types.Named)
 | |
| 		if !ok {
 | |
| 			log.Fatalf("could not find type %s", typeName)
 | |
| 		}
 | |
| 		var hasClone bool
 | |
| 		for i, n := 0, typ.NumMethods(); i < n; i++ {
 | |
| 			if typ.Method(i).Name() == "Clone" {
 | |
| 				hasClone = true
 | |
| 				break
 | |
| 			}
 | |
| 		}
 | |
| 		if !hasClone {
 | |
| 			runCloner = true
 | |
| 		}
 | |
| 		genView(buf, it, typ, fieldComments)
 | |
| 	}
 | |
| 	out := pkg.Name + "_view"
 | |
| 	if *flagBuildTags == "test" {
 | |
| 		out += "_test"
 | |
| 	}
 | |
| 	out += ".go"
 | |
| 	if err := codegen.WritePackageFile("tailscale/cmd/viewer", pkg, out, it, buf); err != nil {
 | |
| 		log.Fatal(err)
 | |
| 	}
 | |
| 	if runCloner {
 | |
| 		// When a new package is added or when existing generated files have
 | |
| 		// been deleted, we might run into a case where tailscale.com/cmd/cloner
 | |
| 		// has not run yet. We detect this by verifying that all the structs we
 | |
| 		// interacted with have had Clone method already generated. If they
 | |
| 		// haven't we ask the caller to rerun generation again so that those get
 | |
| 		// generated.
 | |
| 		log.Printf("%v requires regeneration. Please run go generate again", pkg.Name+"_clone.go")
 | |
| 	}
 | |
| }
 |