Include `dyn.Path` in normalization warnings and errors (#1332)

## Changes

This adds context to warnings and errors. For example:

* Summary: `unknown field bar`
* Location: `foo.yml:6:10`
* Path: `.targets.dev.workspace`

## Tests

Unit tests.
This commit is contained in:
Pieter Noordhuis 2024-04-03 10:56:46 +02:00 committed by GitHub
parent 8c144a2de4
commit c1963ec0df
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 66 additions and 39 deletions

View File

@ -20,6 +20,10 @@ type Diagnostic struct {
// Location is a source code location associated with the diagnostic message. // Location is a source code location associated with the diagnostic message.
// It may be zero if there is no associated location. // It may be zero if there is no associated location.
Location dyn.Location Location dyn.Location
// Path is a path to the value in a configuration tree that the diagnostic is associated with.
// It may be nil if there is no associated path.
Path dyn.Path
} }
// Errorf creates a new error diagnostic. // Errorf creates a new error diagnostic.

View File

@ -33,51 +33,53 @@ func Normalize(dst any, src dyn.Value, opts ...NormalizeOption) (dyn.Value, diag
} }
} }
return n.normalizeType(reflect.TypeOf(dst), src, []reflect.Type{}) return n.normalizeType(reflect.TypeOf(dst), src, []reflect.Type{}, dyn.EmptyPath)
} }
func (n normalizeOptions) normalizeType(typ reflect.Type, src dyn.Value, seen []reflect.Type) (dyn.Value, diag.Diagnostics) { func (n normalizeOptions) normalizeType(typ reflect.Type, src dyn.Value, seen []reflect.Type, path dyn.Path) (dyn.Value, diag.Diagnostics) {
for typ.Kind() == reflect.Pointer { for typ.Kind() == reflect.Pointer {
typ = typ.Elem() typ = typ.Elem()
} }
switch typ.Kind() { switch typ.Kind() {
case reflect.Struct: case reflect.Struct:
return n.normalizeStruct(typ, src, append(seen, typ)) return n.normalizeStruct(typ, src, append(seen, typ), path)
case reflect.Map: case reflect.Map:
return n.normalizeMap(typ, src, append(seen, typ)) return n.normalizeMap(typ, src, append(seen, typ), path)
case reflect.Slice: case reflect.Slice:
return n.normalizeSlice(typ, src, append(seen, typ)) return n.normalizeSlice(typ, src, append(seen, typ), path)
case reflect.String: case reflect.String:
return n.normalizeString(typ, src) return n.normalizeString(typ, src, path)
case reflect.Bool: case reflect.Bool:
return n.normalizeBool(typ, src) return n.normalizeBool(typ, src, path)
case reflect.Int, reflect.Int32, reflect.Int64: case reflect.Int, reflect.Int32, reflect.Int64:
return n.normalizeInt(typ, src) return n.normalizeInt(typ, src, path)
case reflect.Float32, reflect.Float64: case reflect.Float32, reflect.Float64:
return n.normalizeFloat(typ, src) return n.normalizeFloat(typ, src, path)
} }
return dyn.InvalidValue, diag.Errorf("unsupported type: %s", typ.Kind()) return dyn.InvalidValue, diag.Errorf("unsupported type: %s", typ.Kind())
} }
func nullWarning(expected dyn.Kind, src dyn.Value) diag.Diagnostic { func nullWarning(expected dyn.Kind, src dyn.Value, path dyn.Path) diag.Diagnostic {
return diag.Diagnostic{ return diag.Diagnostic{
Severity: diag.Warning, Severity: diag.Warning,
Summary: fmt.Sprintf("expected a %s value, found null", expected), Summary: fmt.Sprintf("expected a %s value, found null", expected),
Location: src.Location(), Location: src.Location(),
Path: path,
} }
} }
func typeMismatch(expected dyn.Kind, src dyn.Value) diag.Diagnostic { func typeMismatch(expected dyn.Kind, src dyn.Value, path dyn.Path) diag.Diagnostic {
return diag.Diagnostic{ return diag.Diagnostic{
Severity: diag.Error, Severity: diag.Error,
Summary: fmt.Sprintf("expected %s, found %s", expected, src.Kind()), Summary: fmt.Sprintf("expected %s, found %s", expected, src.Kind()),
Location: src.Location(), Location: src.Location(),
Path: path,
} }
} }
func (n normalizeOptions) normalizeStruct(typ reflect.Type, src dyn.Value, seen []reflect.Type) (dyn.Value, diag.Diagnostics) { func (n normalizeOptions) normalizeStruct(typ reflect.Type, src dyn.Value, seen []reflect.Type, path dyn.Path) (dyn.Value, diag.Diagnostics) {
var diags diag.Diagnostics var diags diag.Diagnostics
switch src.Kind() { switch src.Kind() {
@ -93,12 +95,13 @@ func (n normalizeOptions) normalizeStruct(typ reflect.Type, src dyn.Value, seen
Severity: diag.Warning, Severity: diag.Warning,
Summary: fmt.Sprintf("unknown field: %s", pk.MustString()), Summary: fmt.Sprintf("unknown field: %s", pk.MustString()),
Location: pk.Location(), Location: pk.Location(),
Path: path,
}) })
continue continue
} }
// Normalize the value according to the field type. // Normalize the value according to the field type.
nv, err := n.normalizeType(typ.FieldByIndex(index).Type, pv, seen) nv, err := n.normalizeType(typ.FieldByIndex(index).Type, pv, seen, path.Append(dyn.Key(pk.MustString())))
if err != nil { if err != nil {
diags = diags.Extend(err) diags = diags.Extend(err)
// Skip the element if it cannot be normalized. // Skip the element if it cannot be normalized.
@ -136,17 +139,17 @@ func (n normalizeOptions) normalizeStruct(typ reflect.Type, src dyn.Value, seen
var v dyn.Value var v dyn.Value
switch ftyp.Kind() { switch ftyp.Kind() {
case reflect.Struct, reflect.Map: case reflect.Struct, reflect.Map:
v, _ = n.normalizeType(ftyp, dyn.V(map[string]dyn.Value{}), seen) v, _ = n.normalizeType(ftyp, dyn.V(map[string]dyn.Value{}), seen, path.Append(dyn.Key(k)))
case reflect.Slice: case reflect.Slice:
v, _ = n.normalizeType(ftyp, dyn.V([]dyn.Value{}), seen) v, _ = n.normalizeType(ftyp, dyn.V([]dyn.Value{}), seen, path.Append(dyn.Key(k)))
case reflect.String: case reflect.String:
v, _ = n.normalizeType(ftyp, dyn.V(""), seen) v, _ = n.normalizeType(ftyp, dyn.V(""), seen, path.Append(dyn.Key(k)))
case reflect.Bool: case reflect.Bool:
v, _ = n.normalizeType(ftyp, dyn.V(false), seen) v, _ = n.normalizeType(ftyp, dyn.V(false), seen, path.Append(dyn.Key(k)))
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
v, _ = n.normalizeType(ftyp, dyn.V(int64(0)), seen) v, _ = n.normalizeType(ftyp, dyn.V(int64(0)), seen, path.Append(dyn.Key(k)))
case reflect.Float32, reflect.Float64: case reflect.Float32, reflect.Float64:
v, _ = n.normalizeType(ftyp, dyn.V(float64(0)), seen) v, _ = n.normalizeType(ftyp, dyn.V(float64(0)), seen, path.Append(dyn.Key(k)))
default: default:
// Skip fields for which we do not have a natural [dyn.Value] equivalent. // Skip fields for which we do not have a natural [dyn.Value] equivalent.
// For example, we don't handle reflect.Complex* and reflect.Uint* types. // For example, we don't handle reflect.Complex* and reflect.Uint* types.
@ -162,10 +165,10 @@ func (n normalizeOptions) normalizeStruct(typ reflect.Type, src dyn.Value, seen
return src, diags return src, diags
} }
return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindMap, src)) return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindMap, src, path))
} }
func (n normalizeOptions) normalizeMap(typ reflect.Type, src dyn.Value, seen []reflect.Type) (dyn.Value, diag.Diagnostics) { func (n normalizeOptions) normalizeMap(typ reflect.Type, src dyn.Value, seen []reflect.Type, path dyn.Path) (dyn.Value, diag.Diagnostics) {
var diags diag.Diagnostics var diags diag.Diagnostics
switch src.Kind() { switch src.Kind() {
@ -176,7 +179,7 @@ func (n normalizeOptions) normalizeMap(typ reflect.Type, src dyn.Value, seen []r
pv := pair.Value pv := pair.Value
// Normalize the value according to the map element type. // Normalize the value according to the map element type.
nv, err := n.normalizeType(typ.Elem(), pv, seen) nv, err := n.normalizeType(typ.Elem(), pv, seen, path.Append(dyn.Key(pk.MustString())))
if err != nil { if err != nil {
diags = diags.Extend(err) diags = diags.Extend(err)
// Skip the element if it cannot be normalized. // Skip the element if it cannot be normalized.
@ -193,10 +196,10 @@ func (n normalizeOptions) normalizeMap(typ reflect.Type, src dyn.Value, seen []r
return src, diags return src, diags
} }
return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindMap, src)) return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindMap, src, path))
} }
func (n normalizeOptions) normalizeSlice(typ reflect.Type, src dyn.Value, seen []reflect.Type) (dyn.Value, diag.Diagnostics) { func (n normalizeOptions) normalizeSlice(typ reflect.Type, src dyn.Value, seen []reflect.Type, path dyn.Path) (dyn.Value, diag.Diagnostics) {
var diags diag.Diagnostics var diags diag.Diagnostics
switch src.Kind() { switch src.Kind() {
@ -204,7 +207,7 @@ func (n normalizeOptions) normalizeSlice(typ reflect.Type, src dyn.Value, seen [
out := make([]dyn.Value, 0, len(src.MustSequence())) out := make([]dyn.Value, 0, len(src.MustSequence()))
for _, v := range src.MustSequence() { for _, v := range src.MustSequence() {
// Normalize the value according to the slice element type. // Normalize the value according to the slice element type.
v, err := n.normalizeType(typ.Elem(), v, seen) v, err := n.normalizeType(typ.Elem(), v, seen, path.Append(dyn.Index(len(out))))
if err != nil { if err != nil {
diags = diags.Extend(err) diags = diags.Extend(err)
// Skip the element if it cannot be normalized. // Skip the element if it cannot be normalized.
@ -221,10 +224,10 @@ func (n normalizeOptions) normalizeSlice(typ reflect.Type, src dyn.Value, seen [
return src, diags return src, diags
} }
return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindSequence, src)) return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindSequence, src, path))
} }
func (n normalizeOptions) normalizeString(typ reflect.Type, src dyn.Value) (dyn.Value, diag.Diagnostics) { func (n normalizeOptions) normalizeString(typ reflect.Type, src dyn.Value, path dyn.Path) (dyn.Value, diag.Diagnostics) {
var diags diag.Diagnostics var diags diag.Diagnostics
var out string var out string
@ -239,15 +242,15 @@ func (n normalizeOptions) normalizeString(typ reflect.Type, src dyn.Value) (dyn.
out = strconv.FormatFloat(src.MustFloat(), 'f', -1, 64) out = strconv.FormatFloat(src.MustFloat(), 'f', -1, 64)
case dyn.KindNil: case dyn.KindNil:
// Return a warning if the field is present but has a null value. // Return a warning if the field is present but has a null value.
return dyn.InvalidValue, diags.Append(nullWarning(dyn.KindString, src)) return dyn.InvalidValue, diags.Append(nullWarning(dyn.KindString, src, path))
default: default:
return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindString, src)) return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindString, src, path))
} }
return dyn.NewValue(out, src.Location()), diags return dyn.NewValue(out, src.Location()), diags
} }
func (n normalizeOptions) normalizeBool(typ reflect.Type, src dyn.Value) (dyn.Value, diag.Diagnostics) { func (n normalizeOptions) normalizeBool(typ reflect.Type, src dyn.Value, path dyn.Path) (dyn.Value, diag.Diagnostics) {
var diags diag.Diagnostics var diags diag.Diagnostics
var out bool var out bool
@ -268,19 +271,19 @@ func (n normalizeOptions) normalizeBool(typ reflect.Type, src dyn.Value) (dyn.Va
} }
// Cannot interpret as a boolean. // Cannot interpret as a boolean.
return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindBool, src)) return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindBool, src, path))
} }
case dyn.KindNil: case dyn.KindNil:
// Return a warning if the field is present but has a null value. // Return a warning if the field is present but has a null value.
return dyn.InvalidValue, diags.Append(nullWarning(dyn.KindBool, src)) return dyn.InvalidValue, diags.Append(nullWarning(dyn.KindBool, src, path))
default: default:
return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindBool, src)) return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindBool, src, path))
} }
return dyn.NewValue(out, src.Location()), diags return dyn.NewValue(out, src.Location()), diags
} }
func (n normalizeOptions) normalizeInt(typ reflect.Type, src dyn.Value) (dyn.Value, diag.Diagnostics) { func (n normalizeOptions) normalizeInt(typ reflect.Type, src dyn.Value, path dyn.Path) (dyn.Value, diag.Diagnostics) {
var diags diag.Diagnostics var diags diag.Diagnostics
var out int64 var out int64
@ -300,19 +303,20 @@ func (n normalizeOptions) normalizeInt(typ reflect.Type, src dyn.Value) (dyn.Val
Severity: diag.Error, Severity: diag.Error,
Summary: fmt.Sprintf("cannot parse %q as an integer", src.MustString()), Summary: fmt.Sprintf("cannot parse %q as an integer", src.MustString()),
Location: src.Location(), Location: src.Location(),
Path: path,
}) })
} }
case dyn.KindNil: case dyn.KindNil:
// Return a warning if the field is present but has a null value. // Return a warning if the field is present but has a null value.
return dyn.InvalidValue, diags.Append(nullWarning(dyn.KindInt, src)) return dyn.InvalidValue, diags.Append(nullWarning(dyn.KindInt, src, path))
default: default:
return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindInt, src)) return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindInt, src, path))
} }
return dyn.NewValue(out, src.Location()), diags return dyn.NewValue(out, src.Location()), diags
} }
func (n normalizeOptions) normalizeFloat(typ reflect.Type, src dyn.Value) (dyn.Value, diag.Diagnostics) { func (n normalizeOptions) normalizeFloat(typ reflect.Type, src dyn.Value, path dyn.Path) (dyn.Value, diag.Diagnostics) {
var diags diag.Diagnostics var diags diag.Diagnostics
var out float64 var out float64
@ -332,13 +336,14 @@ func (n normalizeOptions) normalizeFloat(typ reflect.Type, src dyn.Value) (dyn.V
Severity: diag.Error, Severity: diag.Error,
Summary: fmt.Sprintf("cannot parse %q as a floating point number", src.MustString()), Summary: fmt.Sprintf("cannot parse %q as a floating point number", src.MustString()),
Location: src.Location(), Location: src.Location(),
Path: path,
}) })
} }
case dyn.KindNil: case dyn.KindNil:
// Return a warning if the field is present but has a null value. // Return a warning if the field is present but has a null value.
return dyn.InvalidValue, diags.Append(nullWarning(dyn.KindFloat, src)) return dyn.InvalidValue, diags.Append(nullWarning(dyn.KindFloat, src, path))
default: default:
return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindFloat, src)) return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindFloat, src, path))
} }
return dyn.NewValue(out, src.Location()), diags return dyn.NewValue(out, src.Location()), diags

View File

@ -43,6 +43,7 @@ func TestNormalizeStructElementDiagnostic(t *testing.T) {
Severity: diag.Error, Severity: diag.Error,
Summary: `expected string, found map`, Summary: `expected string, found map`,
Location: dyn.Location{}, Location: dyn.Location{},
Path: dyn.NewPath(dyn.Key("bar")),
}, err[0]) }, err[0])
// Elements that encounter an error during normalization are dropped. // Elements that encounter an error during normalization are dropped.
@ -68,6 +69,7 @@ func TestNormalizeStructUnknownField(t *testing.T) {
Severity: diag.Warning, Severity: diag.Warning,
Summary: `unknown field: bar`, Summary: `unknown field: bar`,
Location: vin.Get("foo").Location(), Location: vin.Get("foo").Location(),
Path: dyn.EmptyPath,
}, err[0]) }, err[0])
// The field that can be mapped to the struct field is retained. // The field that can be mapped to the struct field is retained.
@ -101,6 +103,7 @@ func TestNormalizeStructError(t *testing.T) {
Severity: diag.Error, Severity: diag.Error,
Summary: `expected map, found string`, Summary: `expected map, found string`,
Location: vin.Get("foo").Location(), Location: vin.Get("foo").Location(),
Path: dyn.EmptyPath,
}, err[0]) }, err[0])
} }
@ -245,6 +248,7 @@ func TestNormalizeMapElementDiagnostic(t *testing.T) {
Severity: diag.Error, Severity: diag.Error,
Summary: `expected string, found map`, Summary: `expected string, found map`,
Location: dyn.Location{}, Location: dyn.Location{},
Path: dyn.NewPath(dyn.Key("bar")),
}, err[0]) }, err[0])
// Elements that encounter an error during normalization are dropped. // Elements that encounter an error during normalization are dropped.
@ -270,6 +274,7 @@ func TestNormalizeMapError(t *testing.T) {
Severity: diag.Error, Severity: diag.Error,
Summary: `expected map, found string`, Summary: `expected map, found string`,
Location: vin.Location(), Location: vin.Location(),
Path: dyn.EmptyPath,
}, err[0]) }, err[0])
} }
@ -333,6 +338,7 @@ func TestNormalizeSliceElementDiagnostic(t *testing.T) {
Severity: diag.Error, Severity: diag.Error,
Summary: `expected string, found map`, Summary: `expected string, found map`,
Location: dyn.Location{}, Location: dyn.Location{},
Path: dyn.NewPath(dyn.Index(2)),
}, err[0]) }, err[0])
// Elements that encounter an error during normalization are dropped. // Elements that encounter an error during normalization are dropped.
@ -356,6 +362,7 @@ func TestNormalizeSliceError(t *testing.T) {
Severity: diag.Error, Severity: diag.Error,
Summary: `expected sequence, found string`, Summary: `expected sequence, found string`,
Location: vin.Location(), Location: vin.Location(),
Path: dyn.EmptyPath,
}, err[0]) }, err[0])
} }
@ -410,6 +417,7 @@ func TestNormalizeStringNil(t *testing.T) {
Severity: diag.Warning, Severity: diag.Warning,
Summary: `expected a string value, found null`, Summary: `expected a string value, found null`,
Location: vin.Location(), Location: vin.Location(),
Path: dyn.EmptyPath,
}, err[0]) }, err[0])
} }
@ -446,6 +454,7 @@ func TestNormalizeStringError(t *testing.T) {
Severity: diag.Error, Severity: diag.Error,
Summary: `expected string, found map`, Summary: `expected string, found map`,
Location: dyn.Location{}, Location: dyn.Location{},
Path: dyn.EmptyPath,
}, err[0]) }, err[0])
} }
@ -466,6 +475,7 @@ func TestNormalizeBoolNil(t *testing.T) {
Severity: diag.Warning, Severity: diag.Warning,
Summary: `expected a bool value, found null`, Summary: `expected a bool value, found null`,
Location: vin.Location(), Location: vin.Location(),
Path: dyn.EmptyPath,
}, err[0]) }, err[0])
} }
@ -507,6 +517,7 @@ func TestNormalizeBoolFromStringError(t *testing.T) {
Severity: diag.Error, Severity: diag.Error,
Summary: `expected bool, found string`, Summary: `expected bool, found string`,
Location: vin.Location(), Location: vin.Location(),
Path: dyn.EmptyPath,
}, err[0]) }, err[0])
} }
@ -519,6 +530,7 @@ func TestNormalizeBoolError(t *testing.T) {
Severity: diag.Error, Severity: diag.Error,
Summary: `expected bool, found map`, Summary: `expected bool, found map`,
Location: dyn.Location{}, Location: dyn.Location{},
Path: dyn.EmptyPath,
}, err[0]) }, err[0])
} }
@ -539,6 +551,7 @@ func TestNormalizeIntNil(t *testing.T) {
Severity: diag.Warning, Severity: diag.Warning,
Summary: `expected a int value, found null`, Summary: `expected a int value, found null`,
Location: vin.Location(), Location: vin.Location(),
Path: dyn.EmptyPath,
}, err[0]) }, err[0])
} }
@ -567,6 +580,7 @@ func TestNormalizeIntFromStringError(t *testing.T) {
Severity: diag.Error, Severity: diag.Error,
Summary: `cannot parse "abc" as an integer`, Summary: `cannot parse "abc" as an integer`,
Location: vin.Location(), Location: vin.Location(),
Path: dyn.EmptyPath,
}, err[0]) }, err[0])
} }
@ -579,6 +593,7 @@ func TestNormalizeIntError(t *testing.T) {
Severity: diag.Error, Severity: diag.Error,
Summary: `expected int, found map`, Summary: `expected int, found map`,
Location: dyn.Location{}, Location: dyn.Location{},
Path: dyn.EmptyPath,
}, err[0]) }, err[0])
} }
@ -599,6 +614,7 @@ func TestNormalizeFloatNil(t *testing.T) {
Severity: diag.Warning, Severity: diag.Warning,
Summary: `expected a float value, found null`, Summary: `expected a float value, found null`,
Location: vin.Location(), Location: vin.Location(),
Path: dyn.EmptyPath,
}, err[0]) }, err[0])
} }
@ -627,6 +643,7 @@ func TestNormalizeFloatFromStringError(t *testing.T) {
Severity: diag.Error, Severity: diag.Error,
Summary: `cannot parse "abc" as a floating point number`, Summary: `cannot parse "abc" as a floating point number`,
Location: vin.Location(), Location: vin.Location(),
Path: dyn.EmptyPath,
}, err[0]) }, err[0])
} }
@ -639,5 +656,6 @@ func TestNormalizeFloatError(t *testing.T) {
Severity: diag.Error, Severity: diag.Error,
Summary: `expected float, found map`, Summary: `expected float, found map`,
Location: dyn.Location{}, Location: dyn.Location{},
Path: dyn.EmptyPath,
}, err[0]) }, err[0])
} }