Skip to content

Commit 3893e70

Browse files
committed
all: Simplify the reflect usage
* Use helper funcs in hreflect package when possible. * Use hreflect.ConvertIfPossible to handle conversions when possible. * Move scratch.go from common/maps to common/hstore to clear cyclic import in the next step. * Move Indirect to hreflect and reimplementing it and adusting the behavior to preserve struct pointers. * Adjust evaluateSubElem used by where and others making the struct pointer method case slightly faster.
1 parent b95f7d2 commit 3893e70

29 files changed

+792
-520
lines changed

‎common/collections/append.go‎

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ package collections
1616
import (
1717
"fmt"
1818
"reflect"
19+
20+
"github.com/gohugoio/hugo/common/hreflect"
1921
)
2022

2123
// Append appends from to a slice to and returns the resulting slice.
@@ -25,7 +27,7 @@ func Append(to any, from ...any) (any, error) {
2527
if len(from) == 0 {
2628
return to, nil
2729
}
28-
tov, toIsNil := indirect(reflect.ValueOf(to))
30+
tov, toIsNil := hreflect.Indirect(reflect.ValueOf(to))
2931

3032
toIsNil = toIsNil || to == nil
3133
var tot reflect.Type
@@ -100,7 +102,7 @@ func Append(to any, from ...any) (any, error) {
100102
fv := reflect.ValueOf(f)
101103
if !fv.IsValid() || !fv.Type().AssignableTo(tot) {
102104
// Fall back to a []interface{} slice.
103-
tov, _ := indirect(reflect.ValueOf(to))
105+
tov, _ := hreflect.Indirect(reflect.ValueOf(to))
104106
return appendToInterfaceSlice(tov, from...)
105107
}
106108
tov = reflect.Append(tov, fv)
@@ -136,17 +138,3 @@ func appendToInterfaceSlice(tov reflect.Value, from ...any) ([]any, error) {
136138

137139
return tos, nil
138140
}
139-
140-
// indirect is borrowed from the Go stdlib: 'text/template/exec.go'
141-
// TODO(bep) consolidate
142-
func indirect(v reflect.Value) (rv reflect.Value, isNil bool) {
143-
for ; v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface; v = v.Elem() {
144-
if v.IsNil() {
145-
return v, true
146-
}
147-
if v.Kind() == reflect.Interface && v.NumMethod() > 0 {
148-
break
149-
}
150-
}
151-
return v, false
152-
}

‎common/collections/append_test.go‎

Lines changed: 0 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ package collections
1515

1616
import (
1717
"html/template"
18-
"reflect"
1918
"testing"
2019

2120
qt "github.com/frankban/quicktest"
@@ -148,66 +147,3 @@ func TestAppendShouldMakeACopyOfTheInputSlice(t *testing.T) {
148147
c.Assert(result, qt.DeepEquals, []string{"a", "b", "c"})
149148
c.Assert(slice, qt.DeepEquals, []string{"d", "b"})
150149
}
151-
152-
func TestIndirect(t *testing.T) {
153-
t.Parallel()
154-
c := qt.New(t)
155-
156-
type testStruct struct {
157-
Field string
158-
}
159-
160-
var (
161-
nilPtr *testStruct
162-
nilIface interface{} = nil
163-
nonNilIface interface{} = &testStruct{Field: "hello"}
164-
)
165-
166-
tests := []struct {
167-
name string
168-
input any
169-
wantKind reflect.Kind
170-
wantNil bool
171-
}{
172-
{
173-
name: "nil pointer",
174-
input: nilPtr,
175-
wantKind: reflect.Ptr,
176-
wantNil: true,
177-
},
178-
{
179-
name: "nil interface",
180-
input: nilIface,
181-
wantKind: reflect.Invalid,
182-
wantNil: false,
183-
},
184-
{
185-
name: "non-nil pointer to struct",
186-
input: &testStruct{Field: "abc"},
187-
wantKind: reflect.Struct,
188-
wantNil: false,
189-
},
190-
{
191-
name: "non-nil interface holding pointer",
192-
input: nonNilIface,
193-
wantKind: reflect.Struct,
194-
wantNil: false,
195-
},
196-
{
197-
name: "plain value",
198-
input: testStruct{Field: "xyz"},
199-
wantKind: reflect.Struct,
200-
wantNil: false,
201-
},
202-
}
203-
204-
for _, tt := range tests {
205-
t.Run(tt.name, func(t *testing.T) {
206-
v := reflect.ValueOf(tt.input)
207-
got, isNil := indirect(v)
208-
209-
c.Assert(got.Kind(), qt.Equals, tt.wantKind)
210-
c.Assert(isNil, qt.Equals, tt.wantNil)
211-
})
212-
}
213-
}

‎common/hreflect/convert.go‎

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
// Copyright 2025 The Hugo Authors. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
package hreflect
15+
16+
import (
17+
"fmt"
18+
"math"
19+
"reflect"
20+
)
21+
22+
var (
23+
typeInt64 = reflect.TypeFor[int64]()
24+
typeFloat64 = reflect.TypeFor[float64]()
25+
typeString = reflect.TypeFor[string]()
26+
)
27+
28+
// ToInt64 converts v to int64 if possible, returning an error if not.
29+
func ToInt64E(v reflect.Value) (int64, error) {
30+
if v, ok := ConvertIfPossible(v, typeInt64); ok {
31+
return v.Int(), nil
32+
}
33+
return 0, errConvert(v, "int64")
34+
}
35+
36+
// ToInt64 converts v to int64 if possible. It panics if the conversion is not possible.
37+
func ToInt64(v reflect.Value) int64 {
38+
vv, err := ToInt64E(v)
39+
if err != nil {
40+
panic(err)
41+
}
42+
return vv
43+
}
44+
45+
// ToFloat64E converts v to float64 if possible, returning an error if not.
46+
func ToFloat64E(v reflect.Value) (float64, error) {
47+
if v, ok := ConvertIfPossible(v, typeFloat64); ok {
48+
return v.Float(), nil
49+
}
50+
return 0, errConvert(v, "float64")
51+
}
52+
53+
// ToFloat64 converts v to float64 if possible, panicking if not.
54+
func ToFloat64(v reflect.Value) float64 {
55+
vv, err := ToFloat64E(v)
56+
if err != nil {
57+
panic(err)
58+
}
59+
return vv
60+
}
61+
62+
// ToStringE converts v to string if possible, returning an error if not.
63+
func ToStringE(v reflect.Value) (string, error) {
64+
vv, err := ToStringValueE(v)
65+
if err != nil {
66+
return "", err
67+
}
68+
return vv.String(), nil
69+
}
70+
71+
func ToStringValueE(v reflect.Value) (reflect.Value, error) {
72+
if v, ok := ConvertIfPossible(v, typeString); ok {
73+
return v, nil
74+
}
75+
return reflect.Value{}, errConvert(v, "string")
76+
}
77+
78+
// ToString converts v to string if possible, panicking if not.
79+
func ToString(v reflect.Value) string {
80+
vv, err := ToStringE(v)
81+
if err != nil {
82+
panic(err)
83+
}
84+
return vv
85+
}
86+
87+
func errConvert(v reflect.Value, s string) error {
88+
return fmt.Errorf("unable to convert value of type %q to %q", v.Type().String(), s)
89+
}
90+
91+
// ConvertIfPossible tries to convert val to typ if possible.
92+
// This is currently only implemented for int kinds,
93+
// added to handle the move to a new YAML library which produces uint64 for unsigned integers.
94+
// We can expand on this later if needed.
95+
// This conversion is lossless.
96+
// See Issue 14079.
97+
func ConvertIfPossible(val reflect.Value, typ reflect.Type) (reflect.Value, bool) {
98+
switch val.Kind() {
99+
case reflect.Ptr, reflect.Interface:
100+
if val.IsNil() {
101+
// Return typ's zero value.
102+
return reflect.Zero(typ), true
103+
}
104+
val = val.Elem()
105+
}
106+
107+
if val.Type().AssignableTo(typ) {
108+
// No conversion needed.
109+
return val, true
110+
}
111+
112+
if IsInt(typ.Kind()) {
113+
return convertToIntIfPossible(val, typ)
114+
}
115+
if IsFloat(typ.Kind()) {
116+
return convertToFloatIfPossible(val, typ)
117+
}
118+
if IsUint(typ.Kind()) {
119+
return convertToUintIfPossible(val, typ)
120+
}
121+
if IsString(typ.Kind()) && IsString(val.Kind()) {
122+
return val.Convert(typ), true
123+
}
124+
125+
return reflect.Value{}, false
126+
}
127+
128+
func convertToUintIfPossible(val reflect.Value, typ reflect.Type) (reflect.Value, bool) {
129+
if IsInt(val.Kind()) {
130+
i := val.Int()
131+
if i < 0 {
132+
return reflect.Value{}, false
133+
}
134+
u := uint64(i)
135+
if typ.OverflowUint(u) {
136+
return reflect.Value{}, false
137+
}
138+
return reflect.ValueOf(u).Convert(typ), true
139+
}
140+
if IsUint(val.Kind()) {
141+
if typ.OverflowUint(val.Uint()) {
142+
return reflect.Value{}, false
143+
}
144+
return val.Convert(typ), true
145+
}
146+
if IsFloat(val.Kind()) {
147+
f := val.Float()
148+
if f < 0 || f > float64(math.MaxUint64) {
149+
return reflect.Value{}, false
150+
}
151+
if f != math.Trunc(f) {
152+
return reflect.Value{}, false
153+
}
154+
u := uint64(f)
155+
if typ.OverflowUint(u) {
156+
return reflect.Value{}, false
157+
}
158+
return reflect.ValueOf(u).Convert(typ), true
159+
}
160+
return reflect.Value{}, false
161+
}
162+
163+
func convertToFloatIfPossible(val reflect.Value, typ reflect.Type) (reflect.Value, bool) {
164+
if IsInt(val.Kind()) {
165+
i := val.Int()
166+
f := float64(i)
167+
if typ.OverflowFloat(f) {
168+
return reflect.Value{}, false
169+
}
170+
return reflect.ValueOf(f).Convert(typ), true
171+
}
172+
if IsUint(val.Kind()) {
173+
u := val.Uint()
174+
f := float64(u)
175+
if typ.OverflowFloat(f) {
176+
return reflect.Value{}, false
177+
}
178+
return reflect.ValueOf(f).Convert(typ), true
179+
}
180+
if IsFloat(val.Kind()) {
181+
if typ.OverflowFloat(val.Float()) {
182+
return reflect.Value{}, false
183+
}
184+
return val.Convert(typ), true
185+
}
186+
187+
return reflect.Value{}, false
188+
}
189+
190+
func convertToIntIfPossible(val reflect.Value, typ reflect.Type) (reflect.Value, bool) {
191+
if IsInt(val.Kind()) {
192+
if typ.OverflowInt(val.Int()) {
193+
return reflect.Value{}, false
194+
}
195+
return val.Convert(typ), true
196+
}
197+
if IsUint(val.Kind()) {
198+
if val.Uint() > uint64(math.MaxInt64) {
199+
return reflect.Value{}, false
200+
}
201+
if typ.OverflowInt(int64(val.Uint())) {
202+
return reflect.Value{}, false
203+
}
204+
return val.Convert(typ), true
205+
}
206+
if IsFloat(val.Kind()) {
207+
f := val.Float()
208+
if f < float64(math.MinInt64) || f > float64(math.MaxInt64) {
209+
return reflect.Value{}, false
210+
}
211+
if f != math.Trunc(f) {
212+
return reflect.Value{}, false
213+
}
214+
if typ.OverflowInt(int64(f)) {
215+
return reflect.Value{}, false
216+
}
217+
return reflect.ValueOf(int64(f)).Convert(typ), true
218+
219+
}
220+
221+
return reflect.Value{}, false
222+
}

0 commit comments

Comments
 (0)