Skip to content

Commit d4c7888

Browse files
committed
Fix it so YAML integer types can be used where Go int types are expected
E.g. in date.AddDate. In Hugo v0.152.0 we moved to a new YAML library (github.com/goccy/go-yaml) which produces uint64 for unsigned integers. This unfortunately breaks common constructs like: .Date.AddDate 0 0 7 when .Date is a time.Time and the integers are unmarshaled from YAML front matter. This commit adds code to handle conversion from uint64 (and other int types) to the required int types where possible. Fixes #14079
1 parent 29e2c2f commit d4c7888

File tree

5 files changed

+161
-1
lines changed

5 files changed

+161
-1
lines changed

common/hreflect/helpers.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package hreflect
1818

1919
import (
2020
"context"
21+
"math"
2122
"reflect"
2223
"sync"
2324
"time"
@@ -309,3 +310,29 @@ func IsContextType(tp reflect.Type) bool {
309310
})
310311
return isContext
311312
}
313+
314+
// ConvertIfPossible tries to convert val to typ if possible.
315+
// This is currently only implemented for int kinds,
316+
// added to handle the move to a new YAML library which produces uint64 for unsigned integers.
317+
// We can expand on this later if needed.
318+
// See Issue 14079.
319+
func ConvertIfPossible(val reflect.Value, typ reflect.Type) (reflect.Value, bool) {
320+
if IsInt(typ.Kind()) {
321+
if IsInt(val.Kind()) {
322+
if typ.OverflowInt(val.Int()) {
323+
return reflect.Value{}, false
324+
}
325+
return val.Convert(typ), true
326+
}
327+
if IsUint(val.Kind()) {
328+
if val.Uint() > uint64(math.MaxInt64) {
329+
return reflect.Value{}, false
330+
}
331+
if typ.OverflowInt(int64(val.Uint())) {
332+
return reflect.Value{}, false
333+
}
334+
return val.Convert(typ), true
335+
}
336+
}
337+
return reflect.Value{}, false
338+
}

common/hreflect/helpers_test.go

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

1616
import (
1717
"context"
18+
"math"
1819
"reflect"
1920
"testing"
2021
"time"
@@ -176,3 +177,66 @@ func BenchmarkGetMethodByNamePara(b *testing.B) {
176177
}
177178
})
178179
}
180+
181+
func TestCastIfPossible(t *testing.T) {
182+
c := qt.New(t)
183+
184+
for _, test := range []struct {
185+
name string
186+
value any
187+
typ any
188+
expected any
189+
ok bool
190+
}{
191+
// From uint to int.
192+
{
193+
name: "uint64(math.MaxUint64) to int16",
194+
value: uint64(math.MaxUint64),
195+
typ: int16(0),
196+
ok: false, // overflow
197+
},
198+
199+
{
200+
name: "uint64(math.MaxUint64) to int64",
201+
value: uint64(math.MaxUint64),
202+
typ: int64(0),
203+
ok: false, // overflow
204+
},
205+
{
206+
name: "uint64(math.MaxInt16) to int16",
207+
value: uint64(math.MaxInt16),
208+
typ: int64(0),
209+
ok: true,
210+
expected: int64(math.MaxInt16),
211+
},
212+
// From int to int.
213+
{
214+
name: "int64(math.MaxInt64) to int16",
215+
value: int64(math.MaxInt64),
216+
typ: int16(0),
217+
ok: false, // overflow
218+
},
219+
{
220+
name: "int64(math.MaxInt16) to int",
221+
value: int64(math.MaxInt16),
222+
typ: int(0),
223+
ok: true,
224+
expected: int(math.MaxInt16),
225+
},
226+
227+
{
228+
name: "int64(math.MaxInt16) to int",
229+
value: int64(math.MaxInt16),
230+
typ: int(0),
231+
ok: true,
232+
expected: int(math.MaxInt16),
233+
},
234+
} {
235+
236+
v, ok := ConvertIfPossible(reflect.ValueOf(test.value), reflect.TypeOf(test.typ))
237+
c.Assert(ok, qt.Equals, test.ok, qt.Commentf("test case: %s", test.name))
238+
if test.ok {
239+
c.Assert(v.Interface(), qt.Equals, test.expected, qt.Commentf("test case: %s", test.name))
240+
}
241+
}
242+
}

tpl/internal/go_templates/texttemplate/exec.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -889,7 +889,7 @@ func canBeNil(typ reflect.Type) bool {
889889
}
890890

891891
// validateType guarantees that the value is valid and assignable to the type.
892-
func (s *state) validateType(value reflect.Value, typ reflect.Type) reflect.Value {
892+
func (s *state) _validateType(value reflect.Value, typ reflect.Type) reflect.Value {
893893
if !value.IsValid() {
894894
if typ == nil {
895895
// An untyped nil interface{}. Accept as a proper nil value.

tpl/internal/go_templates/texttemplate/hugo_template.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,55 @@ func (s *state) evalCall(dot, fun reflect.Value, isBuiltin bool, node parse.Node
431431
return vv
432432
}
433433

434+
// validateType guarantees that the value is valid and assignable to the type.
435+
func (s *state) validateType(value reflect.Value, typ reflect.Type) reflect.Value {
436+
if !value.IsValid() {
437+
if typ == nil {
438+
// An untyped nil interface{}. Accept as a proper nil value.
439+
return reflect.ValueOf(nil)
440+
}
441+
if canBeNil(typ) {
442+
// Like above, but use the zero value of the non-nil type.
443+
return reflect.Zero(typ)
444+
}
445+
s.errorf("invalid value; expected %s", typ)
446+
}
447+
if typ == reflectValueType && value.Type() != typ {
448+
return reflect.ValueOf(value)
449+
}
450+
if typ != nil && !value.Type().AssignableTo(typ) {
451+
if value.Kind() == reflect.Interface && !value.IsNil() {
452+
value = value.Elem()
453+
if value.Type().AssignableTo(typ) {
454+
return value
455+
}
456+
// fallthrough
457+
}
458+
// Does one dereference or indirection work? We could do more, as we
459+
// do with method receivers, but that gets messy and method receivers
460+
// are much more constrained, so it makes more sense there than here.
461+
// Besides, one is almost always all you need.
462+
switch {
463+
case value.Kind() == reflect.Pointer && value.Type().Elem().AssignableTo(typ):
464+
value = value.Elem()
465+
if !value.IsValid() {
466+
s.errorf("dereference of nil pointer of type %s", typ)
467+
}
468+
case reflect.PointerTo(value.Type()).AssignableTo(typ) && value.CanAddr():
469+
value = value.Addr()
470+
default:
471+
// Added for Hugo.
472+
if v, ok := hreflect.ConvertIfPossible(value, typ); ok {
473+
value = v
474+
} else {
475+
s.errorf("wrong type for value; expected %s; got %s", typ, value.Type())
476+
}
477+
478+
}
479+
}
480+
return value
481+
}
482+
434483
func isTrue(val reflect.Value) (truth, ok bool) {
435484
return hreflect.IsTruthfulValue(val), true
436485
}

tpl/templates/templates_integration_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,3 +315,23 @@ MyPartial.
315315
b := hugolib.Test(t, files)
316316
b.AssertFileContent("public/index.html", "P1: true|P1: true|")
317317
}
318+
319+
func TestYAMLAddDateIssue14079(t *testing.T) {
320+
t.Parallel()
321+
322+
files := `
323+
-- hugo.toml --
324+
disableKinds = ["page", "section", "taxonomy", "term", "sitemap", "RSS"]
325+
-- assets/mydata.yaml --
326+
myinteger: 2
327+
-- layouts/all.html --
328+
{{ $date := "2023-10-15T13:18:50-07:00" | time }}
329+
{{ $mydata := resources.Get "mydata.yaml" | transform.Unmarshal }}
330+
date: {{ $date | time.Format "2006-01-06" }}|
331+
date+2y: {{ $date.AddDate $mydata.myinteger 0 0 | time.Format "2006-01-06" }}|
332+
`
333+
334+
b := hugolib.Test(t, files)
335+
336+
b.AssertFileContent("public/index.html", "date: 2023-10-23|", "date+2y: 2025-10-25|")
337+
}

0 commit comments

Comments
 (0)