From d4c78885ae4cb7c488ea2ed399aa57f0861f44cb Mon Sep 17 00:00:00 2001 From: =?utf8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Wed, 22 Oct 2025 14:22:59 +0200 Subject: [PATCH] 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 --- common/hreflect/helpers.go | 27 ++++++++ common/hreflect/helpers_test.go | 64 +++++++++++++++++++ .../go_templates/texttemplate/exec.go | 2 +- .../texttemplate/hugo_template.go | 49 ++++++++++++++ tpl/templates/templates_integration_test.go | 20 ++++++ 5 files changed, 161 insertions(+), 1 deletion(-) diff --git a/common/hreflect/helpers.go b/common/hreflect/helpers.go index 81fc0cde4..6aa12de57 100644 --- a/common/hreflect/helpers.go +++ b/common/hreflect/helpers.go @@ -18,6 +18,7 @@ package hreflect import ( "context" + "math" "reflect" "sync" "time" @@ -309,3 +310,29 @@ func IsContextType(tp reflect.Type) bool { }) return isContext } + +// ConvertIfPossible tries to convert val to typ if possible. +// This is currently only implemented for int kinds, +// added to handle the move to a new YAML library which produces uint64 for unsigned integers. +// We can expand on this later if needed. +// See Issue 14079. +func ConvertIfPossible(val reflect.Value, typ reflect.Type) (reflect.Value, bool) { + if IsInt(typ.Kind()) { + if IsInt(val.Kind()) { + if typ.OverflowInt(val.Int()) { + return reflect.Value{}, false + } + return val.Convert(typ), true + } + if IsUint(val.Kind()) { + if val.Uint() > uint64(math.MaxInt64) { + return reflect.Value{}, false + } + if typ.OverflowInt(int64(val.Uint())) { + return reflect.Value{}, false + } + return val.Convert(typ), true + } + } + return reflect.Value{}, false +} diff --git a/common/hreflect/helpers_test.go b/common/hreflect/helpers_test.go index 61361c56c..0b008a44e 100644 --- a/common/hreflect/helpers_test.go +++ b/common/hreflect/helpers_test.go @@ -15,6 +15,7 @@ package hreflect import ( "context" + "math" "reflect" "testing" "time" @@ -176,3 +177,66 @@ func BenchmarkGetMethodByNamePara(b *testing.B) { } }) } + +func TestCastIfPossible(t *testing.T) { + c := qt.New(t) + + for _, test := range []struct { + name string + value any + typ any + expected any + ok bool + }{ + // From uint to int. + { + name: "uint64(math.MaxUint64) to int16", + value: uint64(math.MaxUint64), + typ: int16(0), + ok: false, // overflow + }, + + { + name: "uint64(math.MaxUint64) to int64", + value: uint64(math.MaxUint64), + typ: int64(0), + ok: false, // overflow + }, + { + name: "uint64(math.MaxInt16) to int16", + value: uint64(math.MaxInt16), + typ: int64(0), + ok: true, + expected: int64(math.MaxInt16), + }, + // From int to int. + { + name: "int64(math.MaxInt64) to int16", + value: int64(math.MaxInt64), + typ: int16(0), + ok: false, // overflow + }, + { + name: "int64(math.MaxInt16) to int", + value: int64(math.MaxInt16), + typ: int(0), + ok: true, + expected: int(math.MaxInt16), + }, + + { + name: "int64(math.MaxInt16) to int", + value: int64(math.MaxInt16), + typ: int(0), + ok: true, + expected: int(math.MaxInt16), + }, + } { + + v, ok := ConvertIfPossible(reflect.ValueOf(test.value), reflect.TypeOf(test.typ)) + c.Assert(ok, qt.Equals, test.ok, qt.Commentf("test case: %s", test.name)) + if test.ok { + c.Assert(v.Interface(), qt.Equals, test.expected, qt.Commentf("test case: %s", test.name)) + } + } +} diff --git a/tpl/internal/go_templates/texttemplate/exec.go b/tpl/internal/go_templates/texttemplate/exec.go index f710a25ec..a61fcde19 100644 --- a/tpl/internal/go_templates/texttemplate/exec.go +++ b/tpl/internal/go_templates/texttemplate/exec.go @@ -889,7 +889,7 @@ func canBeNil(typ reflect.Type) bool { } // validateType guarantees that the value is valid and assignable to the type. -func (s *state) validateType(value reflect.Value, typ reflect.Type) reflect.Value { +func (s *state) _validateType(value reflect.Value, typ reflect.Type) reflect.Value { if !value.IsValid() { if typ == nil { // An untyped nil interface{}. Accept as a proper nil value. diff --git a/tpl/internal/go_templates/texttemplate/hugo_template.go b/tpl/internal/go_templates/texttemplate/hugo_template.go index 4f505d8c5..6b0e259c2 100644 --- a/tpl/internal/go_templates/texttemplate/hugo_template.go +++ b/tpl/internal/go_templates/texttemplate/hugo_template.go @@ -431,6 +431,55 @@ func (s *state) evalCall(dot, fun reflect.Value, isBuiltin bool, node parse.Node return vv } +// validateType guarantees that the value is valid and assignable to the type. +func (s *state) validateType(value reflect.Value, typ reflect.Type) reflect.Value { + if !value.IsValid() { + if typ == nil { + // An untyped nil interface{}. Accept as a proper nil value. + return reflect.ValueOf(nil) + } + if canBeNil(typ) { + // Like above, but use the zero value of the non-nil type. + return reflect.Zero(typ) + } + s.errorf("invalid value; expected %s", typ) + } + if typ == reflectValueType && value.Type() != typ { + return reflect.ValueOf(value) + } + if typ != nil && !value.Type().AssignableTo(typ) { + if value.Kind() == reflect.Interface && !value.IsNil() { + value = value.Elem() + if value.Type().AssignableTo(typ) { + return value + } + // fallthrough + } + // Does one dereference or indirection work? We could do more, as we + // do with method receivers, but that gets messy and method receivers + // are much more constrained, so it makes more sense there than here. + // Besides, one is almost always all you need. + switch { + case value.Kind() == reflect.Pointer && value.Type().Elem().AssignableTo(typ): + value = value.Elem() + if !value.IsValid() { + s.errorf("dereference of nil pointer of type %s", typ) + } + case reflect.PointerTo(value.Type()).AssignableTo(typ) && value.CanAddr(): + value = value.Addr() + default: + // Added for Hugo. + if v, ok := hreflect.ConvertIfPossible(value, typ); ok { + value = v + } else { + s.errorf("wrong type for value; expected %s; got %s", typ, value.Type()) + } + + } + } + return value +} + func isTrue(val reflect.Value) (truth, ok bool) { return hreflect.IsTruthfulValue(val), true } diff --git a/tpl/templates/templates_integration_test.go b/tpl/templates/templates_integration_test.go index baa917eee..d923d7c0b 100644 --- a/tpl/templates/templates_integration_test.go +++ b/tpl/templates/templates_integration_test.go @@ -315,3 +315,23 @@ MyPartial. b := hugolib.Test(t, files) b.AssertFileContent("public/index.html", "P1: true|P1: true|") } + +func TestYAMLAddDateIssue14079(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +disableKinds = ["page", "section", "taxonomy", "term", "sitemap", "RSS"] +-- assets/mydata.yaml -- +myinteger: 2 +-- layouts/all.html -- +{{ $date := "2023-10-15T13:18:50-07:00" | time }} +{{ $mydata := resources.Get "mydata.yaml" | transform.Unmarshal }} +date: {{ $date | time.Format "2006-01-06" }}| +date+2y: {{ $date.AddDate $mydata.myinteger 0 0 | time.Format "2006-01-06" }}| +` + + b := hugolib.Test(t, files) + + b.AssertFileContent("public/index.html", "date: 2023-10-23|", "date+2y: 2025-10-25|") +} -- 2.39.5