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|") +}