5
0
mirror of https://github.com/wailsapp/wails.git synced 2025-05-07 16:50:19 +08:00
wails/v3/internal/generator/collect/model.go
Fabio Massaioli 37673eb24d
[v3] Fix binding generator bugs and prepare for Go 1.24 (#4045)
* 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>
2025-02-09 09:44:34 +11:00

382 lines
11 KiB
Go

package collect
import (
"cmp"
"go/ast"
"go/constant"
"go/types"
"slices"
"strings"
"sync"
)
type (
// ModelInfo records all information that is required
// to render JS/TS code for a model type.
//
// Read accesses to exported fields are only safe
// if a call to [ModelInfo.Collect] has completed before the access,
// for example by calling it in the accessing goroutine
// or before spawning the accessing goroutine.
ModelInfo struct {
*TypeInfo
// Internal records whether the model
// should be exported by the index file.
Internal bool
// Imports records dependencies for this model.
Imports *ImportMap
// Type records the target type for an alias or derived model,
// the underlying type for an enum.
Type types.Type
// Fields records the property list for a class or struct alias model,
// in order of declaration and grouped by their declaring [ast.Field].
Fields [][]*ModelFieldInfo
// Values records the value list for an enum model,
// in order of declaration and grouped
// by their declaring [ast.GenDecl] and [ast.ValueSpec].
Values [][][]*ConstInfo
// TypeParams records type parameter names for generic models.
TypeParams []string
// Predicates caches the value of all type predicates for this model.
//
// WARN: whenever working with a generic uninstantiated model type,
// use these instead of invoking predicate functions,
// which may incur a large performance penalty.
Predicates Predicates
collector *Collector
once sync.Once
}
// ModelFieldInfo holds extended information
// about a struct field in a model type.
ModelFieldInfo struct {
*StructField
*FieldInfo
}
// Predicates caches the value of all type predicates.
Predicates struct {
IsJSONMarshaler MarshalerKind
MaybeJSONMarshaler MarshalerKind
IsTextMarshaler MarshalerKind
MaybeTextMarshaler MarshalerKind
IsMapKey bool
IsTypeParam bool
IsStringAlias bool
IsClass bool
IsAny bool
}
)
func newModelInfo(collector *Collector, obj *types.TypeName) *ModelInfo {
return &ModelInfo{
TypeInfo: collector.Type(obj),
collector: collector,
}
}
// Model retrieves the the unique [ModelInfo] instance
// associated to the given type object within a Collector.
// If none is present, Model initialises a new one
// registers it for code generation
// and schedules background collection activity.
//
// Model is safe for concurrent use.
func (collector *Collector) Model(obj *types.TypeName) *ModelInfo {
pkg := collector.Package(obj.Pkg())
if pkg == nil {
return nil
}
model, present := pkg.recordModel(obj)
if !present {
collector.scheduler.Schedule(func() { model.Collect() })
}
return model
}
// Collect gathers information for the model described by its receiver.
// It can be called concurrently by multiple goroutines;
// the computation will be performed just once.
//
// Collect returns the receiver for chaining.
// It is safe to call Collect with nil receiver.
//
// After Collect returns, the calling goroutine and all goroutines
// it might spawn afterwards are free to access
// the receiver's fields indefinitely.
func (info *ModelInfo) Collect() *ModelInfo {
if info == nil {
return nil
}
// Changes in the following logic must be reflected adequately
// by the predicates in properties.go, by ImportMap.AddType
// and by all render.Module methods.
info.once.Do(func() {
collector := info.collector
obj := info.Object().(*types.TypeName)
typ := obj.Type()
// Collect type information.
info.TypeInfo.Collect()
// Initialise import map.
info.Imports = NewImportMap(collector.Package(obj.Pkg()))
// Setup fallback type.
info.Type = types.Universe.Lookup("any").Type()
// Record whether the model should be exported.
info.Internal = !obj.Exported()
// Parse directives.
for _, doc := range []*ast.CommentGroup{info.Doc, info.Decl.Doc} {
if doc == nil {
continue
}
for _, comment := range doc.List {
if IsDirective(comment.Text, "internal") {
info.Internal = true
}
}
}
// Record type parameter names.
var isGeneric bool
if generic, ok := typ.(interface{ TypeParams() *types.TypeParamList }); ok {
tparams := generic.TypeParams()
isGeneric = tparams != nil
if isGeneric && tparams.Len() > 0 {
info.TypeParams = make([]string, tparams.Len())
for i := range tparams.Len() {
info.TypeParams[i] = tparams.At(i).Obj().Name()
}
}
}
// Precompute predicates.
// Preinstantiate typ to avoid repeated instantiations in predicate code.
ityp := instantiate(typ)
info.Predicates = Predicates{
IsJSONMarshaler: IsJSONMarshaler(ityp),
MaybeJSONMarshaler: MaybeJSONMarshaler(ityp),
IsTextMarshaler: IsTextMarshaler(ityp),
MaybeTextMarshaler: MaybeTextMarshaler(ityp),
IsMapKey: IsMapKey(ityp),
IsTypeParam: IsTypeParam(ityp),
IsStringAlias: IsStringAlias(ityp),
IsClass: IsClass(ityp),
IsAny: IsAny(ityp),
}
var def types.Type
var constants []*types.Const
switch t := typ.(type) {
case *types.Alias:
// Model is an alias: take rhs as definition.
// It is important not to skip alias chains with [types.Unalias]
// because in doing so we could end up with a private type from another package.
def = t.Rhs()
// Test for constants with alias type,
// but only when non-generic alias resolves to a basic type
// (hence not to e.g. a named type).
if basic, ok := types.Unalias(def).(*types.Basic); ok {
if !isGeneric && basic.Info()&types.IsConstType != 0 && basic.Info()&types.IsComplex == 0 {
// Non-generic alias resolves to a representable constant type:
// look for defined constants whose type is exactly the alias typ.
for _, name := range obj.Pkg().Scope().Names() {
if cnst, ok := obj.Pkg().Scope().Lookup(name).(*types.Const); ok {
alias, isAlias := cnst.Type().(*types.Alias)
if isAlias && cnst.Val().Kind() != constant.Unknown && alias.Obj() == t.Obj() {
constants = append(constants, cnst)
}
}
}
}
}
case *types.Named:
// Model is a named type:
// jump directly to underlying type to match go semantics,
// i.e. do not render named types as aliases for other named types.
def = typ.Underlying()
// Check whether it implements marshaler interfaces or has defined constants.
if info.Predicates.MaybeJSONMarshaler != NonMarshaler {
// Type marshals to a custom value of unknown shape.
// If it has explicit custom marshaling logic, render it as any;
// otherwise, delegate to the underlying type that must be the actual [json.Marshaler].
if info.Predicates.MaybeJSONMarshaler == ExplicitMarshaler {
return
}
} else if info.Predicates.MaybeTextMarshaler != NonMarshaler {
// Type marshals to a custom string of unknown shape.
// If it has explicit custom marshaling logic, render it as string;
// otherwise, delegate to the underlying type that must be the actual [encoding.TextMarshaler].
//
// One exception must be made for situations
// where the underlying type is a [json.Marshaler] but the model is not:
// in that case, we cannot delegate to the underlying type either.
// Note that in such a case the underlying type is never a pointer or interface,
// because those cannot have explicitly defined methods,
// hence it would not possible for the model not to be a [json.Marshaler]
// while the underlying type is.
if info.Predicates.MaybeTextMarshaler == ExplicitMarshaler || MaybeJSONMarshaler(def) != NonMarshaler {
info.Type = types.Typ[types.String]
return
}
} else if basic, ok := def.Underlying().(*types.Basic); ok {
// Test for enums (excluding marshalers and generic types).
if !isGeneric && basic.Info()&types.IsConstType != 0 && basic.Info()&types.IsComplex == 0 {
// Named type is defined as a representable constant type:
// look for defined constants of that named type.
for _, name := range obj.Pkg().Scope().Names() {
if cnst, ok := obj.Pkg().Scope().Lookup(name).(*types.Const); ok {
if cnst.Val().Kind() != constant.Unknown && types.Identical(cnst.Type(), typ) {
constants = append(constants, cnst)
}
}
}
}
}
default:
panic("model has unknown object kind (neither alias nor named type)")
}
// Handle struct types.
strct, isStruct := def.(*types.Struct)
if isStruct && info.Predicates.MaybeJSONMarshaler == NonMarshaler && info.Predicates.MaybeTextMarshaler == NonMarshaler {
// Def is struct and model is not a marshaler:
// collect information about struct fields.
info.collectStruct(strct)
info.Type = nil
return
}
// Record required imports.
info.Imports.AddType(def)
// Handle enum types.
// constants slice is always empty for structs, marshalers.
if len(constants) > 0 {
// Collect information about enum values.
info.collectEnum(constants)
}
// That's all, folks. Render as a TS alias.
info.Type = def
})
return info
}
// collectEnum collects information about enum values and their declarations.
func (info *ModelInfo) collectEnum(constants []*types.Const) {
// Collect information about each constant object.
values := make([]*ConstInfo, len(constants))
for i, cnst := range constants {
values[i] = info.collector.Const(cnst).Collect()
}
// Sort values by grouping and source order.
slices.SortFunc(values, func(v1 *ConstInfo, v2 *ConstInfo) int {
// Skip comparisons for identical pointers.
if v1 == v2 {
return 0
}
// Sort first by source order of declaration group.
if v1.Decl != v2.Decl {
return cmp.Compare(v1.Decl.Pos, v2.Decl.Pos)
}
// Then by source order of spec.
if v1.Spec != v2.Spec {
return cmp.Compare(v1.Spec.Pos, v2.Spec.Pos)
}
// Then by source order of identifiers.
if v1.Pos != v2.Pos {
return cmp.Compare(v1.Pos, v2.Pos)
}
// Finally by name (for constants whose source position is unknown).
return strings.Compare(v1.Name, v2.Name)
})
// Split value list into groups and subgroups.
var decl, spec *GroupInfo
decli, speci := -1, -1
for _, value := range values {
if value.Spec != spec {
spec = value.Spec
if value.Decl == decl {
speci++
} else {
decl = value.Decl
decli++
speci = 0
info.Values = append(info.Values, nil)
}
info.Values[decli] = append(info.Values[decli], nil)
}
info.Values[decli][speci] = append(info.Values[decli][speci], value)
}
}
// collectStruct collects information about struct fields and their declarations.
func (info *ModelInfo) collectStruct(strct *types.Struct) {
collector := info.collector
// Retrieve struct info.
structInfo := collector.Struct(strct).Collect()
// Allocate result slice.
fields := make([]*ModelFieldInfo, len(structInfo.Fields))
// Collect fields.
for i, field := range structInfo.Fields {
// Record required imports.
info.Imports.AddType(field.Type)
fields[i] = &ModelFieldInfo{
StructField: field,
FieldInfo: collector.Field(field.Object).Collect(),
}
}
// Split field list into groups, preserving the original order.
var decl *GroupInfo
decli := -1
for _, field := range fields {
if field.Decl != decl {
decl = field.Decl
decli++
info.Fields = append(info.Fields, nil)
}
info.Fields[decli] = append(info.Fields[decli], field)
}
}