Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 28 additions & 17 deletions internal/builders/resolvers/assertions.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ package resolvers
import (
"fmt"
"go/ast"
"go/importer"
"go/token"
"go/types"

oaispec "github.com/go-openapi/spec"
Expand Down Expand Up @@ -75,23 +75,34 @@ func IsFieldStringable(tpe ast.Expr) bool {
return false
}

func IsTextMarshaler(tpe types.Type) bool {
encoding, err := importer.Default().Import("encoding")
if err != nil {
return false
}

iface := encoding.Scope().Lookup("TextMarshaler")
if iface == nil {
return false
}

asInterface, ok := iface.Type().Underlying().(*types.Interface)
if !ok {
return false
}
// textMarshalerIface is the encoding.TextMarshaler interface, synthesized with go/types:
//
// interface{ MarshalText() (text []byte, err error) }
//
// It is built structurally rather than by importing the real "encoding" package through
// go/importer's default importer. That importer resolves stdlib export data by running
// "go list -export" against the GOROOT the binary was built against. When GOTOOLCHAIN
// selects a different toolchain at runtime — or the binary simply runs on a machine whose Go
// installation lives at a different path than the build machine's — that invocation fails
// ("cannot find main module"), the error is silently swallowed, and every TextMarshaler check
// returns false. types.Implements compares method sets structurally, so a synthesized,
// structurally identical interface is equivalent to the imported one and removes the runtime
// dependency on a working "go list".
var textMarshalerIface = types.NewInterfaceType([]*types.Func{ //nolint:gochecknoglobals // immutable synthesized interface, built once, read-only
types.NewFunc(token.NoPos, nil, "MarshalText",
types.NewSignatureType(nil, nil, nil,
nil,
types.NewTuple(
types.NewVar(token.NoPos, nil, "text", types.NewSlice(types.Typ[types.Byte])),
types.NewVar(token.NoPos, nil, "err", types.Universe.Lookup("error").Type()),
),
false,
),
),
}, nil).Complete()

return types.Implements(tpe, asInterface)
func IsTextMarshaler(tpe types.Type) bool {
return types.Implements(tpe, textMarshalerIface)
}

// IsJSONMapKey reports whether a Go map with this key type marshals to a JSON object under
Expand Down
44 changes: 44 additions & 0 deletions internal/builders/resolvers/resolvers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,50 @@ func TestParseFieldTag(t *testing.T) {
})
}

func TestIsTextMarshaler(t *testing.T) {
pkg := types.NewPackage("example.com/x", "x")

// namedWithMarshalText builds a named struct type carrying a value-receiver method
// MarshalText whose results are the supplied tuple.
namedWithMarshalText := func(name string, results *types.Tuple) *types.Named {
tn := types.NewTypeName(token.NoPos, pkg, name, nil)
named := types.NewNamed(tn, types.NewStruct(nil, nil), nil)
recv := types.NewVar(token.NoPos, pkg, "m", named)
sig := types.NewSignatureType(recv, nil, nil, types.NewTuple(), results, false)
named.AddMethod(types.NewFunc(token.NoPos, pkg, "MarshalText", sig))
return named
}

bytesAndError := types.NewTuple(
types.NewVar(token.NoPos, nil, "", types.NewSlice(types.Typ[types.Byte])),
types.NewVar(token.NoPos, nil, "", types.Universe.Lookup("error").Type()),
)

t.Run("MarshalText() ([]byte, error) satisfies the interface", func(t *testing.T) {
named := namedWithMarshalText("Marshaler", bytesAndError)
assert.TrueT(t, IsTextMarshaler(named))
// A value-receiver method promotes to the pointer method set too.
assert.TrueT(t, IsTextMarshaler(types.NewPointer(named)))
})

t.Run("type without MarshalText does not", func(t *testing.T) {
tn := types.NewTypeName(token.NoPos, pkg, "Plain", nil)
named := types.NewNamed(tn, types.NewStruct(nil, nil), nil)
assert.FalseT(t, IsTextMarshaler(named))
})

t.Run("wrong signature (no error result) does not", func(t *testing.T) {
onlyBytes := types.NewTuple(
types.NewVar(token.NoPos, nil, "", types.NewSlice(types.Typ[types.Byte])),
)
assert.FalseT(t, IsTextMarshaler(namedWithMarshalText("Bad", onlyBytes)))
})

t.Run("builtin string does not", func(t *testing.T) {
assert.FalseT(t, IsTextMarshaler(types.Typ[types.String]))
})
}

func TestMustNotBeABuiltinType(t *testing.T) {
t.Run("user type does not panic", func(t *testing.T) {
pkg := types.NewPackage("example.com/foo", "foo")
Expand Down
Loading