mirror of
https://github.com/wailsapp/wails.git
synced 2025-05-08 00:53:33 +08:00

* Rename predicates source file * Overhaul and document type predicates * Fix model collection logic for named types * Fix map key type rendering * Fix map creation code * Fix rendering of structs that implement marshaler interfaces * Fix type cycle detection to take type args into account * Fix enum and typeparam field initialisation * Improve unsupported type warnings * Remove internal models file * Deduplicate template code * Accept generic aliases in static analyser * Support new `encoding/json` flag `omitzero` * Handle special cases when rendering generic aliases * Update npm test dependencies * Test class aliases and implicit private dependencies * Test marshaler combinations * Test map key types * Remove bad map keys from unrelated tests * Test service discovery through generic aliases * Test generic aliases * Test warning messages * Disable go1.24 tests * Update changelog * Restore rendering of injected lines in index file * Test directives * Add wails:ignore directive * Fix typo * Move injections to the bottom of service files * Handle errors from closing files * Do not emit messages when services define only lifecycle methods * Add internal directive for services and models * Update changelog * Fix error in service templates * Test internal directive on services/models * Fix error in index template * Base testdata updates * Testdata for class aliases and implicit private dependencies * Testdata for marshaler combinations * Testdata for map key types * Testdata for bad map key fixes * Add weakly typed enums aka alias constants * Testdata for enum and typeparam field fixes * Testdata for generic aliases * Testdata for warning messages * Testdata for directives * Testdata for weakly typed enums * Update binding example * Update services example * Remove go1.24 testdata * Update cli doc * Fix analyser tests * Fix windows tests... hopefully * go mod tidy on examples * Update bindings guide --------- Co-authored-by: Lea Anthony <lea.anthony@gmail.com>
479 lines
16 KiB
Go
479 lines
16 KiB
Go
package collect
|
|
|
|
// This file gathers functions that test useful properties of model types.
|
|
// The rationale for the way things are handled here
|
|
// is given in the example file found at ./_reference/json_marshaler_behaviour.go
|
|
|
|
import (
|
|
"go/token"
|
|
"go/types"
|
|
"iter"
|
|
|
|
"golang.org/x/exp/typeparams"
|
|
)
|
|
|
|
// Cached interface types.
|
|
var (
|
|
ifaceTextMarshaler = types.NewInterfaceType([]*types.Func{
|
|
types.NewFunc(token.NoPos, nil, "MarshalText",
|
|
types.NewSignatureType(nil, nil, nil, types.NewTuple(), types.NewTuple(
|
|
types.NewParam(token.NoPos, nil, "", types.NewSlice(types.Universe.Lookup("byte").Type())),
|
|
types.NewParam(token.NoPos, nil, "", types.Universe.Lookup("error").Type()),
|
|
), false)),
|
|
}, nil).Complete()
|
|
|
|
ifaceJSONMarshaler = types.NewInterfaceType([]*types.Func{
|
|
types.NewFunc(token.NoPos, nil, "MarshalJSON",
|
|
types.NewSignatureType(nil, nil, nil, types.NewTuple(), types.NewTuple(
|
|
types.NewParam(token.NoPos, nil, "", types.NewSlice(types.Universe.Lookup("byte").Type())),
|
|
types.NewParam(token.NoPos, nil, "", types.Universe.Lookup("error").Type()),
|
|
), false)),
|
|
}, nil).Complete()
|
|
)
|
|
|
|
// MarshalerKind values describe
|
|
// whether and how a type implements a marshaler interface.
|
|
// For any one of the two marshaler interfaces, a type is
|
|
// - a NonMarshaler if it does not implement it;
|
|
// - an ImplicitMarshaler if it inherits the implementation from its underlying type;
|
|
// - an ExplicitMarshaler if it defines the relevant method explicitly.
|
|
type MarshalerKind byte
|
|
|
|
const (
|
|
NonMarshaler MarshalerKind = iota
|
|
ImplicitMarshaler
|
|
ExplicitMarshaler
|
|
)
|
|
|
|
// termlist returns an iterator over the normalised term list of the given type.
|
|
// If typ is invalid or has an empty type set, termlist returns the empty sequence.
|
|
// If typ has an empty term list
|
|
// then termlist returns a sequence with just one element: the type itself.
|
|
//
|
|
// TODO: replace with new term set API once Go 1.25 is out.
|
|
// See go.dev/issue/61013
|
|
func termlist(typ types.Type) iter.Seq[*typeparams.Term] {
|
|
terms, err := typeparams.NormalTerms(types.Unalias(typ))
|
|
return func(yield func(*typeparams.Term) bool) {
|
|
if err == nil && len(terms) == 0 {
|
|
yield(typeparams.NewTerm(false, typ))
|
|
} else {
|
|
for _, term := range terms {
|
|
if !yield(term) {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// instantiate instantiates typ if it is an uninstantiated generic type
|
|
// using its own type parameters as arguments in order to preserve genericity.
|
|
//
|
|
// If typ is not generic or already instantiated, it is returned as is.
|
|
// If typ is not an alias, then the returned type is not an alias either.
|
|
func instantiate(typ types.Type) types.Type {
|
|
if t, ok := typ.(interface {
|
|
TypeParams() *types.TypeParamList
|
|
TypeArgs() *types.TypeList
|
|
}); ok && t.TypeParams() != nil && t.TypeArgs() == nil {
|
|
args := make([]types.Type, t.TypeParams().Len())
|
|
for i := range args {
|
|
args[i] = t.TypeParams().At(i)
|
|
}
|
|
|
|
typ, _ = types.Instantiate(nil, typ, args, false)
|
|
}
|
|
|
|
return typ
|
|
}
|
|
|
|
// isMarshaler checks whether the given type
|
|
// implements one of the two marshaler interfaces,
|
|
// and whether it implements it explicitly,
|
|
// i.e. by defining the relevant method directly
|
|
// instead of inheriting it from the underlying type.
|
|
//
|
|
// If addressable is true, it checks both pointer and non-pointer receivers.
|
|
//
|
|
// The behaviour of isMarshaler is unspecified
|
|
// if marshaler is not one of [json.Marshaler] or [encoding.TextMarshaler].
|
|
func isMarshaler(typ types.Type, marshaler *types.Interface, addressable bool, visited map[*types.TypeName]MarshalerKind) MarshalerKind {
|
|
// Follow alias chain and instantiate if necessary.
|
|
//
|
|
// types.Implements does not handle generics,
|
|
// hence when typ is generic it must be instantiated.
|
|
//
|
|
// Instantiation operations may incur a large performance penalty and are usually cached,
|
|
// but doing so here would entail some complex global state and a potential memory leak.
|
|
// Because typ should be generic only during model collection,
|
|
// it should be enough to cache the result of marshaler queries for models.
|
|
typ = instantiate(types.Unalias(typ))
|
|
|
|
// Invariant: at this point, typ is not an alias.
|
|
|
|
if typ == types.Typ[types.Invalid] {
|
|
// Do not pass invalid types to [types.Implements].
|
|
return NonMarshaler
|
|
}
|
|
|
|
result := types.Implements(typ, marshaler)
|
|
|
|
ptr, isPtr := typ.Underlying().(*types.Pointer)
|
|
|
|
if !result && addressable && !isPtr {
|
|
result = types.Implements(types.NewPointer(typ), marshaler)
|
|
}
|
|
|
|
named, isNamed := typ.(*types.Named)
|
|
|
|
if result {
|
|
// Check whether marshaler method is implemented explicitly on a named type.
|
|
if isNamed {
|
|
method := marshaler.Method(0).Name()
|
|
for i := range named.NumMethods() {
|
|
if named.Method(i).Name() == method {
|
|
return ExplicitMarshaler
|
|
}
|
|
}
|
|
}
|
|
|
|
return ImplicitMarshaler
|
|
}
|
|
|
|
// Fast path: named types that fail the [types.Implements] test cannot be marshalers.
|
|
//
|
|
// WARN: currently typeparams cannot be used on the rhs of a named type declaration.
|
|
// If that changes in the future,
|
|
// this guard will become essential for correctness,
|
|
// not just a shortcut.
|
|
if isNamed {
|
|
return NonMarshaler
|
|
}
|
|
|
|
// Unwrap at most one pointer and follow alias chain.
|
|
if isPtr {
|
|
typ = types.Unalias(ptr.Elem())
|
|
}
|
|
|
|
// Invariant: at this point, typ is not an alias.
|
|
|
|
// Type parameters require special handling:
|
|
// iterate over their term list and treat them as marshalers
|
|
// if so are all their potential instantiations.
|
|
|
|
tp, ok := typ.(*types.TypeParam)
|
|
if !ok {
|
|
return NonMarshaler
|
|
}
|
|
|
|
// Init cycle detection/deduplication map.
|
|
if visited == nil {
|
|
visited = make(map[*types.TypeName]MarshalerKind)
|
|
}
|
|
|
|
// Type params cannot be embedded in constraints directly,
|
|
// but they can be embedded as pointer terms.
|
|
//
|
|
// When we hit that kind of cycle,
|
|
// we can err towards it being a marshaler:
|
|
// such a constraint is meaningless anyways,
|
|
// as no type can be simultaneously a pointer to itself.
|
|
//
|
|
// Therefore, we iterate the type set
|
|
// only for unvisited pointers-to-typeparams,
|
|
// and return the current best guess
|
|
// for those we have already visited.
|
|
//
|
|
// WARN: there has been some talk
|
|
// of allowing type parameters as embedded fields/terms.
|
|
// That might make our lives miserable here.
|
|
// The spec must be monitored for changes in that regard.
|
|
if isPtr {
|
|
if kind, ok := visited[tp.Obj()]; ok {
|
|
return kind
|
|
}
|
|
}
|
|
|
|
// Initialise kind to explicit marshaler, then decrease as needed.
|
|
kind := ExplicitMarshaler
|
|
|
|
if isPtr {
|
|
// Pointers are never explicit marshalers.
|
|
kind = ImplicitMarshaler
|
|
// Mark pointer-to-typeparam as visited and init current best guess.
|
|
visited[tp.Obj()] = kind
|
|
}
|
|
|
|
// Iterate term list.
|
|
for term := range termlist(tp) {
|
|
ttyp := types.Unalias(term.Type())
|
|
|
|
// Reject if tp has a tilde or invalid element in its term list
|
|
// or has a method-only constraint.
|
|
//
|
|
// Valid tilde terms
|
|
// can always be satisfied by named types that hide their methods
|
|
// hence fail in general to implement the required interface.
|
|
if term.Tilde() || ttyp == types.Typ[types.Invalid] || ttyp == tp {
|
|
kind = NonMarshaler
|
|
break
|
|
}
|
|
|
|
// Propagate the presence of a wrapping pointer.
|
|
if isPtr {
|
|
ttyp = types.NewPointer(ttyp)
|
|
}
|
|
|
|
kind = min(kind, isMarshaler(ttyp, marshaler, addressable && !isPtr, visited))
|
|
if kind == NonMarshaler {
|
|
// We can stop here as we've reached the minimum [MarshalerKind].
|
|
break
|
|
}
|
|
}
|
|
|
|
// Store final response for pointer-to-typeparam.
|
|
if isPtr {
|
|
visited[tp.Obj()] = kind
|
|
}
|
|
|
|
return kind
|
|
}
|
|
|
|
// IsTextMarshaler queries whether and how the given type
|
|
// implements the [encoding.TextMarshaler] interface.
|
|
func IsTextMarshaler(typ types.Type) MarshalerKind {
|
|
return isMarshaler(typ, ifaceTextMarshaler, false, nil)
|
|
}
|
|
|
|
// MaybeTextMarshaler queries whether and how the given type
|
|
// implements the [encoding.TextMarshaler] interface for at least one receiver form.
|
|
func MaybeTextMarshaler(typ types.Type) MarshalerKind {
|
|
return isMarshaler(typ, ifaceTextMarshaler, true, nil)
|
|
}
|
|
|
|
// IsJSONMarshaler queries whether and how the given type
|
|
// implements the [json.Marshaler] interface.
|
|
func IsJSONMarshaler(typ types.Type) MarshalerKind {
|
|
return isMarshaler(typ, ifaceJSONMarshaler, false, nil)
|
|
}
|
|
|
|
// MaybeJSONMarshaler queries whether and how the given type
|
|
// implements the [json.Marshaler] interface for at least one receiver form.
|
|
func MaybeJSONMarshaler(typ types.Type) MarshalerKind {
|
|
return isMarshaler(typ, ifaceJSONMarshaler, true, nil)
|
|
}
|
|
|
|
// IsMapKey returns true if the given type
|
|
// is accepted as a map key by encoding/json.
|
|
func IsMapKey(typ types.Type) bool {
|
|
// Iterate over type set and return true if all elements are valid.
|
|
//
|
|
// We cannot simply delegate to [IsTextMarshaler] here
|
|
// because a union of some basic terms and some TextMarshalers
|
|
// might still be acceptable.
|
|
//
|
|
// NOTE: If typ is not a typeparam or constraint, termlist returns just typ itself.
|
|
// If typ has an empty type set, it's safe to return true
|
|
// because the map cannot be instantiated anyways.
|
|
for term := range termlist(typ) {
|
|
ttyp := types.Unalias(term.Type())
|
|
|
|
// Types whose underlying type is a signed/unsigned integer or a string
|
|
// are always acceptable, whether they are marshalers or not.
|
|
if basic, ok := ttyp.Underlying().(*types.Basic); ok {
|
|
if basic.Info()&(types.IsInteger|types.IsUnsigned|types.IsString) != 0 {
|
|
continue
|
|
}
|
|
}
|
|
|
|
// Valid tilde terms
|
|
// can always be satisfied by named types that hide their methods
|
|
// hence fail in general to implement the required interface.
|
|
// For example one could have:
|
|
//
|
|
// type NotAKey struct{ encoding.TextMarshaler }
|
|
// func (NotAKey) MarshalText() int { ... }
|
|
//
|
|
// which satisfies ~struct{ encoding.TextMarshaler }
|
|
// but is not itself a TextMarshaler.
|
|
//
|
|
// It might still be the case that the constraint
|
|
// requires explicitly a marshaling method,
|
|
// hence we perform one last check on typ.
|
|
//
|
|
// For example, we reject interface{ ~struct{ ... } }
|
|
// but still accept interface{ ~struct{ ... }; MarshalText() ([]byte, error) }
|
|
//
|
|
// All other cases are only acceptable
|
|
// if the type implements [encoding.TextMarshaler] in non-addressable mode.
|
|
if term.Tilde() || IsTextMarshaler(ttyp) == NonMarshaler {
|
|
// When some term fails, test the input typ itself,
|
|
// but only if it has not been tested already.
|
|
//
|
|
// Note that when term.Tilde() is true
|
|
// then it is always the case that typ != term.Type(),
|
|
// because cyclic constraints are not allowed
|
|
// and naked type parameters cannot occur in type unions.
|
|
return typ != term.Type() && IsTextMarshaler(typ) != NonMarshaler
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// IsTypeParam returns true when the given type
|
|
// is either a TypeParam or a pointer to a TypeParam.
|
|
func IsTypeParam(typ types.Type) bool {
|
|
switch t := types.Unalias(typ).(type) {
|
|
case *types.TypeParam:
|
|
return true
|
|
case *types.Pointer:
|
|
_, ok := types.Unalias(t.Elem()).(*types.TypeParam)
|
|
return ok
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// IsStringAlias returns true when
|
|
// either typ will be rendered to JS/TS as an alias for the TS type `string`,
|
|
// or typ itself (not its underlying type) is a pointer
|
|
// whose element type satisfies the property described above.
|
|
//
|
|
// This predicate is only safe to use either with map keys,
|
|
// where pointers are treated in an ad-hoc way by [json.Marshal],
|
|
// or when typ IS ALREADY KNOWN to be either [types.Alias] or [types.Named].
|
|
//
|
|
// Otherwise, the result might be incorrect:
|
|
// IsStringAlias MUST NOT be used to check
|
|
// whether an arbitrary instance of [types.Type]
|
|
// renders as a JS/TS string type.
|
|
//
|
|
// Notice that IsStringAlias returns false for all type parameters:
|
|
// detecting those that must be always instantiated as string aliases
|
|
// is technically possible, but very difficult.
|
|
func IsStringAlias(typ types.Type) bool {
|
|
// Unwrap at most one pointer.
|
|
// NOTE: do not unalias typ before testing:
|
|
// aliases whose underlying type is a pointer
|
|
// are never rendered as strings.
|
|
if ptr, ok := typ.(*types.Pointer); ok {
|
|
typ = ptr.Elem()
|
|
}
|
|
|
|
switch typ.(type) {
|
|
case *types.Alias, *types.Named:
|
|
// Aliases and named types might be rendered as string aliases.
|
|
default:
|
|
// Not a model type, hence not an alias.
|
|
return false
|
|
}
|
|
|
|
// Skip pointer and interface types: they are always nullable
|
|
// and cannot have any explicitly defined methods.
|
|
// This takes care of rejecting type params as well,
|
|
// since their underlying type is guaranteed to be an interface.
|
|
switch typ.Underlying().(type) {
|
|
case *types.Pointer, *types.Interface:
|
|
return false
|
|
}
|
|
|
|
// Follow alias chain.
|
|
typ = types.Unalias(typ)
|
|
|
|
// Aliases of the basic string type are rendered as strings.
|
|
if basic, ok := typ.(*types.Basic); ok {
|
|
return basic.Info()&types.IsString != 0
|
|
}
|
|
|
|
// json.Marshalers can only be rendered as any.
|
|
// TextMarshalers that aren't json.Marshalers render as strings.
|
|
if MaybeJSONMarshaler(typ) != NonMarshaler {
|
|
return false
|
|
} else if MaybeTextMarshaler(typ) != NonMarshaler {
|
|
return true
|
|
}
|
|
|
|
// Named types whose underlying type is a string are rendered as strings.
|
|
basic, ok := typ.Underlying().(*types.Basic)
|
|
return ok && basic.Info()&types.IsString != 0
|
|
}
|
|
|
|
// IsClass returns true if the given type will be rendered
|
|
// as a JS/TS model class (or interface).
|
|
func IsClass(typ types.Type) bool {
|
|
// Follow alias chain.
|
|
typ = types.Unalias(typ)
|
|
|
|
if _, isNamed := typ.(*types.Named); !isNamed {
|
|
// Unnamed types are never rendered as classes.
|
|
return false
|
|
}
|
|
|
|
// Struct named types without custom marshaling are rendered as classes.
|
|
_, isStruct := typ.Underlying().(*types.Struct)
|
|
return isStruct && MaybeJSONMarshaler(typ) == NonMarshaler && MaybeTextMarshaler(typ) == NonMarshaler
|
|
}
|
|
|
|
// IsAny returns true if the given type
|
|
// is guaranteed to render as the TS any type or equivalent.
|
|
//
|
|
// It might return false negatives for generic aliases,
|
|
// hence should only be used with instantiated types
|
|
// or in contexts where false negatives are acceptable.
|
|
func IsAny(typ types.Type) bool {
|
|
// Follow alias chain.
|
|
typ = types.Unalias(typ)
|
|
|
|
if MaybeJSONMarshaler(typ) != NonMarshaler {
|
|
// If typ is either a named type, an interface, a pointer or a struct,
|
|
// it will be rendered as (possibly an alias for) the TS any type.
|
|
//
|
|
// If it is a type parameter that implements json.Marshal,
|
|
// every possible concrete instantiation will implement json.Marshal,
|
|
// hence will be rendered as the TS any type.
|
|
return true
|
|
}
|
|
|
|
if MaybeTextMarshaler(typ) != NonMarshaler {
|
|
// If type is either a named type, an interface, a pointer or a struct,
|
|
// it will be rendered as (possibly an alias for)
|
|
// the (possibly nullable) TS string type.
|
|
//
|
|
// If typ is a type parameter, we know at this point
|
|
// that it does not necessarily implement json.Marshaler,
|
|
// hence it will be possible to instantiate it in a way
|
|
// that renders as the (possibly nullable) TS string type.
|
|
return false
|
|
}
|
|
|
|
if ptr, ok := typ.Underlying().(*types.Pointer); ok {
|
|
// Pointers render as the union of their element type with null.
|
|
// This is equivalent to the TS any type
|
|
// if and only if so is the element type.
|
|
return IsAny(ptr.Elem())
|
|
}
|
|
|
|
// All types listed below have rich TS equivalents,
|
|
// hence won't be equivalent to the TS any type.
|
|
//
|
|
// WARN: it is important to keep these lists explicit and up to date
|
|
// instead of listing the unsupported types (which would be much easier).
|
|
//
|
|
// By doing so, IsAny will keep working correctly
|
|
// in case future updates to the Go spec introduce new type families,
|
|
// thus buying the maintainers some time to patch the binding generator.
|
|
|
|
// Retrieve underlying type.
|
|
switch t := typ.Underlying().(type) {
|
|
case *types.Basic:
|
|
// Complex types are not supported.
|
|
return t.Info()&(types.IsBoolean|types.IsInteger|types.IsUnsigned|types.IsFloat|types.IsString) == 0
|
|
case *types.Array, *types.Slice, *types.Map, *types.Struct, *types.TypeParam:
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|