Skip to content

Commit 6f7b04a

Browse files
authored
feat: lift interface{} params to Ref<T> for reflection decoders (#219)
1 parent 1266fde commit 6f7b04a

12 files changed

Lines changed: 248 additions & 14 deletions

File tree

bindgen/bindgen.external.json

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1813,6 +1813,44 @@
18131813
"periph.io/x/conn/v3/pin": [
18141814
"*"
18151815
]
1816+
},
1817+
"reflection_decode": {
1818+
"github.com/spf13/viper": [
1819+
"Unmarshal",
1820+
"UnmarshalKey",
1821+
"UnmarshalExact",
1822+
"Viper.Unmarshal",
1823+
"Viper.UnmarshalKey",
1824+
"Viper.UnmarshalExact"
1825+
],
1826+
"gopkg.in/yaml.v3": [
1827+
"Unmarshal",
1828+
"Decoder.Decode"
1829+
],
1830+
"gopkg.in/yaml.v2": [
1831+
"Unmarshal",
1832+
"UnmarshalStrict",
1833+
"Decoder.Decode"
1834+
],
1835+
"github.com/goccy/go-yaml": [
1836+
"Unmarshal",
1837+
"UnmarshalWithOptions",
1838+
"Decoder.Decode"
1839+
],
1840+
"github.com/BurntSushi/toml": [
1841+
"Unmarshal",
1842+
"Decoder.Decode"
1843+
],
1844+
"github.com/pelletier/go-toml/v2": [
1845+
"Unmarshal",
1846+
"Decoder.Decode"
1847+
],
1848+
"github.com/go-viper/mapstructure/v2": [
1849+
"Decode",
1850+
"WeakDecode",
1851+
"DecodeMetadata",
1852+
"WeakDecodeMetadata"
1853+
]
18161854
}
18171855
}
18181856
}

bindgen/bindgen.stdlib.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,12 @@
379379
"image/color": ["*"],
380380
"io/ioutil": ["Discard"],
381381
"unicode": ["*"]
382+
},
383+
"reflection_decode": {
384+
"encoding/json": ["Unmarshal", "Decoder.Decode"],
385+
"encoding/xml": ["Unmarshal", "Decoder.Decode", "Decoder.DecodeElement"],
386+
"encoding/gob": ["Decoder.Decode"],
387+
"encoding/binary": ["Read"]
382388
}
383389
}
384390
}

bindgen/internal/config/config.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ type TypeOverrides struct {
3939
// "absent" with `-1`. Bindgen rewrites the return to `Option<int>`
4040
// and emits `#[go(sentinel_minus_one)]`.
4141
SentinelMinusOne map[string][]string `json:"sentinel_minus_one"`
42+
// ReflectionDecode declares functions whose `interface{}` params reach
43+
// Go reflection; each such param is lifted to a fresh `T` and rewritten
44+
// to `Ref<T>`.
45+
ReflectionDecode map[string][]string `json:"reflection_decode"`
4246
}
4347

4448
// LoadConfig loads bindgen configuration from the given path.
@@ -195,6 +199,20 @@ func (c *Config) SentinelInt(pkg, name string) (int, bool) {
195199
return 0, false
196200
}
197201

202+
// IsReflectionDecode reports whether the given function or method is
203+
// configured to lift its `interface{}` params to `Ref<T>`. Uses "Type.Method"
204+
// dot notation for methods.
205+
func (c *Config) IsReflectionDecode(pkg, name string) bool {
206+
if c == nil {
207+
return false
208+
}
209+
names, ok := lookupWithGlob(c.Overrides.Types.ReflectionDecode, pkg)
210+
if !ok {
211+
return false
212+
}
213+
return matchesWildcard(names, name)
214+
}
215+
198216
// IsNeverReturn returns true if the given function or method in the given
199217
// package never returns normally (e.g., os.Exit, log.Fatal).
200218
func (c *Config) IsNeverReturn(pkg, name string) bool {

bindgen/internal/convert/converters.go

Lines changed: 73 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,62 @@ func isReferenceType(typeStr string) bool {
3939
return strings.HasPrefix(typeStr, "Slice<") || strings.HasPrefix(typeStr, "Map<")
4040
}
4141

42+
// liftReflectionDecodeParams returns (specs, nil) when not whitelisted or no
43+
// `interface{}` params are liftable; the index map encodes per-param Ref<T>
44+
// rewrites for the caller to apply during the param loop.
45+
func (c *Converter) liftReflectionDecodeParams(
46+
sig *types.Signature,
47+
qualifiedName string,
48+
specs TypeParamSpecs,
49+
) (TypeParamSpecs, map[int]string) {
50+
if !c.cfg.IsReflectionDecode(c.currentPkgPath, qualifiedName) {
51+
return specs, nil
52+
}
53+
used := make(map[string]bool, len(specs))
54+
for _, s := range specs {
55+
used[s.Name] = true
56+
}
57+
var overrides map[int]string
58+
params := sig.Params()
59+
for i := 0; i < params.Len(); i++ {
60+
if sig.Variadic() && i == params.Len()-1 {
61+
continue
62+
}
63+
t := params.At(i).Type()
64+
for {
65+
alias, ok := t.(*types.Alias)
66+
if !ok {
67+
break
68+
}
69+
t = alias.Rhs()
70+
}
71+
iface, ok := t.(*types.Interface)
72+
if !ok || !iface.Empty() || isErrorInterface(iface) {
73+
continue
74+
}
75+
name := freshTypeParamName(used)
76+
used[name] = true
77+
specs = append(specs, TypeParamSpec{Name: name})
78+
if overrides == nil {
79+
overrides = make(map[int]string)
80+
}
81+
overrides[i] = fmt.Sprintf("Ref<%s>", name)
82+
}
83+
return specs, overrides
84+
}
85+
86+
func freshTypeParamName(used map[string]bool) string {
87+
if !used["T"] {
88+
return "T"
89+
}
90+
for n := 2; ; n++ {
91+
candidate := fmt.Sprintf("T%d", n)
92+
if !used[candidate] {
93+
return candidate
94+
}
95+
}
96+
}
97+
4298
func (c *Converter) convertFunction(result *ConvertResult, symbolExport extract.SymbolExport) {
4399
signature, ok := symbolExport.GoType.(*types.Signature)
44100
if !ok {
@@ -58,6 +114,9 @@ func (c *Converter) convertFunction(result *ConvertResult, symbolExport extract.
58114
c.typeParamSubstitutions = substitutions
59115
defer func() { c.typeParamSubstitutions = prevSubs }()
60116

117+
liftedSpecs, paramOverrides := c.liftReflectionDecodeParams(signature, result.Name, result.TypeParams)
118+
result.TypeParams = liftedSpecs
119+
61120
mutParams := c.cfg.MutatingParams(c.currentPkgPath, result.Name)
62121

63122
params := signature.Params()
@@ -73,6 +132,9 @@ func (c *Converter) convertFunction(result *ConvertResult, symbolExport extract.
73132
if signature.Variadic() && i == params.Len()-1 {
74133
typeStr = sliceToVarArgs(typeStr)
75134
}
135+
if override, ok := paramOverrides[i]; ok {
136+
typeStr = override
137+
}
76138

77139
name := param.Name()
78140
if name == "" {
@@ -204,6 +266,13 @@ func (c *Converter) convertMethod(result *ConvertResult, symbolExport extract.Sy
204266
qualifiedName = result.Receiver.BaseTypeName + "." + result.Name
205267
}
206268

269+
methodSpecs, _, skip := collectTypeParams(signature.TypeParams(), false, c)
270+
if skip != nil {
271+
result.SkipReason = skip
272+
return
273+
}
274+
liftedSpecs, paramOverrides := c.liftReflectionDecodeParams(signature, qualifiedName, methodSpecs)
275+
207276
mutParams := c.cfg.MutatingParams(c.currentPkgPath, qualifiedName)
208277

209278
params := signature.Params()
@@ -219,6 +288,9 @@ func (c *Converter) convertMethod(result *ConvertResult, symbolExport extract.Sy
219288
if signature.Variadic() && i == params.Len()-1 {
220289
typeStr = sliceToVarArgs(typeStr)
221290
}
291+
if override, ok := paramOverrides[i]; ok {
292+
typeStr = override
293+
}
222294

223295
name := param.Name()
224296
if name == "" {
@@ -290,12 +362,7 @@ func (c *Converter) convertMethod(result *ConvertResult, symbolExport extract.Sy
290362
}
291363
}
292364

293-
methodSpecs, _, skip := collectTypeParams(signature.TypeParams(), false, c)
294-
if skip != nil {
295-
result.SkipReason = skip
296-
return
297-
}
298-
result.TypeParams = methodSpecs
365+
result.TypeParams = liftedSpecs
299366

300367
if isFluentBuilderCandidate(result, symbolExport, signature) {
301368
if fn := c.findFuncDecl(symbolExport.Obj); fn != nil && isFluentMethod(fn, ncGetReceiverName(fn)) {

bindgen/tests/bindgen_test.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"testing"
1111

1212
"github.com/ivov/lisette/bindgen/internal/cli"
13+
"github.com/ivov/lisette/bindgen/internal/config"
1314
)
1415

1516
var update = flag.Bool("update", false, "update snapshot files")
@@ -84,7 +85,17 @@ func snapshotPathFor(fixturePath string) string {
8485
}
8586

8687
func runBindgen(t *testing.T, pkgPath string) []byte {
87-
result, err := cli.GeneratePkg("./"+pkgPath, "0.0.0", "0.0.0", nil)
88+
var cfg *config.Config
89+
cfgPath := filepath.Join(pkgPath, "bindgen.json")
90+
if _, err := os.Stat(cfgPath); err == nil {
91+
loaded, err := config.LoadConfig(cfgPath, nil)
92+
if err != nil {
93+
t.Fatalf("failed to load fixture config %s: %v", cfgPath, err)
94+
}
95+
cfg = &loaded
96+
}
97+
98+
result, err := cli.GeneratePkg("./"+pkgPath, "0.0.0", "0.0.0", cfg)
8899
if err != nil {
89100
t.Fatalf("bindgen failed: %v", err)
90101
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"overrides": {
3+
"types": {
4+
"reflection_decode": {
5+
"github.com/ivov/lisette/bindgen/tests/testdata/fixtures/reflection_decode": [
6+
"Unmarshal",
7+
"UnmarshalKey",
8+
"DecodeBoth",
9+
"ScanRow",
10+
"DecodeWithErr",
11+
"DecodeAny",
12+
"Decoder.Decode",
13+
"Codec.Unmarshal"
14+
]
15+
}
16+
}
17+
}
18+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package reflection_decode
2+
3+
// Pointer-out shape: json.Unmarshal-like.
4+
func Unmarshal(data []byte, v interface{}) error { return nil }
5+
6+
// Key + pointer-out shape: viper.UnmarshalKey-like.
7+
func UnmarshalKey(key string, v interface{}) error { return nil }
8+
9+
// Multiple interface{} params: each lifts to its own T_n.
10+
func DecodeBoth(src interface{}, dst interface{}) error { return nil }
11+
12+
// Variadic interface{} is intentionally NOT lifted (sql.Rows.Scan-shape).
13+
func ScanRow(dest ...interface{}) error { return nil }
14+
15+
// Method on a value receiver.
16+
type Decoder struct{}
17+
18+
func (d Decoder) Decode(v interface{}) error { return nil }
19+
20+
// Method on a pointer receiver, plus a non-whitelisted method that should
21+
// keep its `interface{}` param as `Unknown`.
22+
type Codec struct{}
23+
24+
func (c *Codec) Unmarshal(data []byte, v interface{}) error { return nil }
25+
26+
func (c *Codec) Set(v interface{}) {}
27+
28+
// Non-whitelisted free function with `interface{}` — stays `Unknown`.
29+
func Log(v interface{}) {}
30+
31+
// `error` interface must NOT be lifted, even when whitelisted.
32+
func DecodeWithErr(v interface{}, err error) error { return err }
33+
34+
// Param typed as `any` (alias for `interface{}`) must also be lifted.
35+
func DecodeAny(v any) error { return nil }
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Generated by Lisette bindgen
2+
// Source: ./testdata/fixtures/reflection_decode (local)
3+
// Go: 0.0.0
4+
// Lisette: 0.0.0
5+
6+
/// Param typed as `any` (alias for `interface{}`) must also be lifted.
7+
pub fn DecodeAny<T>(v: Ref<T>) -> Result<(), error>
8+
9+
/// Multiple interface{} params: each lifts to its own T_n.
10+
pub fn DecodeBoth<T, T2>(src: Ref<T>, dst: Ref<T2>) -> Result<(), error>
11+
12+
/// `error` interface must NOT be lifted, even when whitelisted.
13+
pub fn DecodeWithErr<T>(v: Ref<T>, err: error) -> Result<(), error>
14+
15+
/// Non-whitelisted free function with `interface{}` — stays `Unknown`.
16+
pub fn Log(v: Unknown)
17+
18+
/// Variadic interface{} is intentionally NOT lifted (sql.Rows.Scan-shape).
19+
pub fn ScanRow(dest: VarArgs<Unknown>) -> Result<(), error>
20+
21+
/// Pointer-out shape: json.Unmarshal-like.
22+
pub fn Unmarshal<T>(data: Slice<uint8>, v: Ref<T>) -> Result<(), error>
23+
24+
/// Key + pointer-out shape: viper.UnmarshalKey-like.
25+
pub fn UnmarshalKey<T>(key: string, v: Ref<T>) -> Result<(), error>
26+
27+
/// Method on a pointer receiver, plus a non-whitelisted method that should
28+
/// keep its `interface{}` param as `Unknown`.
29+
pub type Codec
30+
31+
/// Method on a value receiver.
32+
pub type Decoder
33+
34+
impl Codec {
35+
fn Set(self: Ref<Codec>, v: Unknown)
36+
fn Unmarshal<T>(self: Ref<Codec>, data: Slice<uint8>, v: Ref<T>) -> Result<(), error>
37+
}
38+
39+
impl Decoder {
40+
fn Decode<T>(self, v: Ref<T>) -> Result<(), error>
41+
}

crates/stdlib/typedefs/encoding/binary.d.lis

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ pub fn PutVarint(mut buf: Slice<uint8>, x: int64) -> int
5555
/// The error is [io.EOF] only if no bytes were read.
5656
/// If an [io.EOF] happens after reading some but not all the bytes,
5757
/// Read returns [io.ErrUnexpectedEOF].
58-
pub fn Read(r: io.Reader, order: ByteOrder, data: Unknown) -> Result<(), error>
58+
pub fn Read<T>(r: io.Reader, order: ByteOrder, data: Ref<T>) -> Result<(), error>
5959

6060
/// ReadUvarint reads an encoded unsigned integer from r and returns it as a uint64.
6161
/// The error is [io.EOF] only if no bytes were read.

crates/stdlib/typedefs/encoding/gob.d.lis

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ impl Decoder {
7474
/// correct type for the next data item received.
7575
/// If the input is at EOF, Decode returns [io.EOF] and
7676
/// does not modify e.
77-
fn Decode(self: Ref<Decoder>, e: Unknown) -> Result<(), error>
77+
fn Decode<T>(self: Ref<Decoder>, e: Ref<T>) -> Result<(), error>
7878

7979
/// DecodeValue reads the next value from the input stream.
8080
/// If v is the zero reflect.Value (v.Kind() == Invalid), DecodeValue discards the value.

0 commit comments

Comments
 (0)