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
import (
"context"
+ "math"
"reflect"
"sync"
"time"
})
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
+}
import (
"context"
+ "math"
"reflect"
"testing"
"time"
}
})
}
+
+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))
+ }
+ }
+}
}
// 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.
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
}
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|")
+}