]> git.maquefel.me Git - brevno-suite/hugo/commitdiff
Fix it so YAML integer types can be used where Go int types are expected
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Wed, 22 Oct 2025 12:22:59 +0000 (14:22 +0200)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Wed, 22 Oct 2025 19:04:39 +0000 (21:04 +0200)
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
common/hreflect/helpers_test.go
tpl/internal/go_templates/texttemplate/exec.go
tpl/internal/go_templates/texttemplate/hugo_template.go
tpl/templates/templates_integration_test.go

index 81fc0cde47e5770d50b296e762e7f7fcf45a5c54..6aa12de573a5f58b465725e3143c6e58cbf39471 100644 (file)
@@ -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
+}
index 61361c56c28e425b4356d419abeb853e61a33da3..0b008a44ec56192a0c488a1b329c6aceef615ae2 100644 (file)
@@ -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))
+               }
+       }
+}
index f710a25ec2c37c36370647a18417755a66df9d11..a61fcde191b53e27671b812d36cbd7908b92ba81 100644 (file)
@@ -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.
index 4f505d8c55e818cfa1bf0a3fe82c67899a565f2f..6b0e259c2312217872d54a26c52de411ed78bd19 100644 (file)
@@ -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
 }
index baa917eeed1a030852fbc887a47757e6ea76a5ee..d923d7c0bf72d1bd457972c5f99e52d6edc17ba8 100644 (file)
@@ -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|")
+}