Skip to content

Commit 7d09e5e

Browse files
authored
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.
1 parent 21af90e commit 7d09e5e

3 files changed

Lines changed: 66 additions & 39 deletions

File tree

libs/diag/diagnostic.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ type Diagnostic struct {
2020
// Location is a source code location associated with the diagnostic message.
2121
// It may be zero if there is no associated location.
2222
Location dyn.Location
23+
24+
// Path is a path to the value in a configuration tree that the diagnostic is associated with.
25+
// It may be nil if there is no associated path.
26+
Path dyn.Path
2327
}
2428

2529
// Errorf creates a new error diagnostic.

libs/dyn/convert/normalize.go

Lines changed: 44 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -33,51 +33,53 @@ func Normalize(dst any, src dyn.Value, opts ...NormalizeOption) (dyn.Value, diag
3333
}
3434
}
3535

36-
return n.normalizeType(reflect.TypeOf(dst), src, []reflect.Type{})
36+
return n.normalizeType(reflect.TypeOf(dst), src, []reflect.Type{}, dyn.EmptyPath)
3737
}
3838

39-
func (n normalizeOptions) normalizeType(typ reflect.Type, src dyn.Value, seen []reflect.Type) (dyn.Value, diag.Diagnostics) {
39+
func (n normalizeOptions) normalizeType(typ reflect.Type, src dyn.Value, seen []reflect.Type, path dyn.Path) (dyn.Value, diag.Diagnostics) {
4040
for typ.Kind() == reflect.Pointer {
4141
typ = typ.Elem()
4242
}
4343

4444
switch typ.Kind() {
4545
case reflect.Struct:
46-
return n.normalizeStruct(typ, src, append(seen, typ))
46+
return n.normalizeStruct(typ, src, append(seen, typ), path)
4747
case reflect.Map:
48-
return n.normalizeMap(typ, src, append(seen, typ))
48+
return n.normalizeMap(typ, src, append(seen, typ), path)
4949
case reflect.Slice:
50-
return n.normalizeSlice(typ, src, append(seen, typ))
50+
return n.normalizeSlice(typ, src, append(seen, typ), path)
5151
case reflect.String:
52-
return n.normalizeString(typ, src)
52+
return n.normalizeString(typ, src, path)
5353
case reflect.Bool:
54-
return n.normalizeBool(typ, src)
54+
return n.normalizeBool(typ, src, path)
5555
case reflect.Int, reflect.Int32, reflect.Int64:
56-
return n.normalizeInt(typ, src)
56+
return n.normalizeInt(typ, src, path)
5757
case reflect.Float32, reflect.Float64:
58-
return n.normalizeFloat(typ, src)
58+
return n.normalizeFloat(typ, src, path)
5959
}
6060

6161
return dyn.InvalidValue, diag.Errorf("unsupported type: %s", typ.Kind())
6262
}
6363

64-
func nullWarning(expected dyn.Kind, src dyn.Value) diag.Diagnostic {
64+
func nullWarning(expected dyn.Kind, src dyn.Value, path dyn.Path) diag.Diagnostic {
6565
return diag.Diagnostic{
6666
Severity: diag.Warning,
6767
Summary: fmt.Sprintf("expected a %s value, found null", expected),
6868
Location: src.Location(),
69+
Path: path,
6970
}
7071
}
7172

72-
func typeMismatch(expected dyn.Kind, src dyn.Value) diag.Diagnostic {
73+
func typeMismatch(expected dyn.Kind, src dyn.Value, path dyn.Path) diag.Diagnostic {
7374
return diag.Diagnostic{
7475
Severity: diag.Error,
7576
Summary: fmt.Sprintf("expected %s, found %s", expected, src.Kind()),
7677
Location: src.Location(),
78+
Path: path,
7779
}
7880
}
7981

80-
func (n normalizeOptions) normalizeStruct(typ reflect.Type, src dyn.Value, seen []reflect.Type) (dyn.Value, diag.Diagnostics) {
82+
func (n normalizeOptions) normalizeStruct(typ reflect.Type, src dyn.Value, seen []reflect.Type, path dyn.Path) (dyn.Value, diag.Diagnostics) {
8183
var diags diag.Diagnostics
8284

8385
switch src.Kind() {
@@ -93,12 +95,13 @@ func (n normalizeOptions) normalizeStruct(typ reflect.Type, src dyn.Value, seen
9395
Severity: diag.Warning,
9496
Summary: fmt.Sprintf("unknown field: %s", pk.MustString()),
9597
Location: pk.Location(),
98+
Path: path,
9699
})
97100
continue
98101
}
99102

100103
// Normalize the value according to the field type.
101-
nv, err := n.normalizeType(typ.FieldByIndex(index).Type, pv, seen)
104+
nv, err := n.normalizeType(typ.FieldByIndex(index).Type, pv, seen, path.Append(dyn.Key(pk.MustString())))
102105
if err != nil {
103106
diags = diags.Extend(err)
104107
// Skip the element if it cannot be normalized.
@@ -136,17 +139,17 @@ func (n normalizeOptions) normalizeStruct(typ reflect.Type, src dyn.Value, seen
136139
var v dyn.Value
137140
switch ftyp.Kind() {
138141
case reflect.Struct, reflect.Map:
139-
v, _ = n.normalizeType(ftyp, dyn.V(map[string]dyn.Value{}), seen)
142+
v, _ = n.normalizeType(ftyp, dyn.V(map[string]dyn.Value{}), seen, path.Append(dyn.Key(k)))
140143
case reflect.Slice:
141-
v, _ = n.normalizeType(ftyp, dyn.V([]dyn.Value{}), seen)
144+
v, _ = n.normalizeType(ftyp, dyn.V([]dyn.Value{}), seen, path.Append(dyn.Key(k)))
142145
case reflect.String:
143-
v, _ = n.normalizeType(ftyp, dyn.V(""), seen)
146+
v, _ = n.normalizeType(ftyp, dyn.V(""), seen, path.Append(dyn.Key(k)))
144147
case reflect.Bool:
145-
v, _ = n.normalizeType(ftyp, dyn.V(false), seen)
148+
v, _ = n.normalizeType(ftyp, dyn.V(false), seen, path.Append(dyn.Key(k)))
146149
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
147-
v, _ = n.normalizeType(ftyp, dyn.V(int64(0)), seen)
150+
v, _ = n.normalizeType(ftyp, dyn.V(int64(0)), seen, path.Append(dyn.Key(k)))
148151
case reflect.Float32, reflect.Float64:
149-
v, _ = n.normalizeType(ftyp, dyn.V(float64(0)), seen)
152+
v, _ = n.normalizeType(ftyp, dyn.V(float64(0)), seen, path.Append(dyn.Key(k)))
150153
default:
151154
// Skip fields for which we do not have a natural [dyn.Value] equivalent.
152155
// 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
162165
return src, diags
163166
}
164167

165-
return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindMap, src))
168+
return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindMap, src, path))
166169
}
167170

168-
func (n normalizeOptions) normalizeMap(typ reflect.Type, src dyn.Value, seen []reflect.Type) (dyn.Value, diag.Diagnostics) {
171+
func (n normalizeOptions) normalizeMap(typ reflect.Type, src dyn.Value, seen []reflect.Type, path dyn.Path) (dyn.Value, diag.Diagnostics) {
169172
var diags diag.Diagnostics
170173

171174
switch src.Kind() {
@@ -176,7 +179,7 @@ func (n normalizeOptions) normalizeMap(typ reflect.Type, src dyn.Value, seen []r
176179
pv := pair.Value
177180

178181
// Normalize the value according to the map element type.
179-
nv, err := n.normalizeType(typ.Elem(), pv, seen)
182+
nv, err := n.normalizeType(typ.Elem(), pv, seen, path.Append(dyn.Key(pk.MustString())))
180183
if err != nil {
181184
diags = diags.Extend(err)
182185
// Skip the element if it cannot be normalized.
@@ -193,18 +196,18 @@ func (n normalizeOptions) normalizeMap(typ reflect.Type, src dyn.Value, seen []r
193196
return src, diags
194197
}
195198

196-
return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindMap, src))
199+
return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindMap, src, path))
197200
}
198201

199-
func (n normalizeOptions) normalizeSlice(typ reflect.Type, src dyn.Value, seen []reflect.Type) (dyn.Value, diag.Diagnostics) {
202+
func (n normalizeOptions) normalizeSlice(typ reflect.Type, src dyn.Value, seen []reflect.Type, path dyn.Path) (dyn.Value, diag.Diagnostics) {
200203
var diags diag.Diagnostics
201204

202205
switch src.Kind() {
203206
case dyn.KindSequence:
204207
out := make([]dyn.Value, 0, len(src.MustSequence()))
205208
for _, v := range src.MustSequence() {
206209
// Normalize the value according to the slice element type.
207-
v, err := n.normalizeType(typ.Elem(), v, seen)
210+
v, err := n.normalizeType(typ.Elem(), v, seen, path.Append(dyn.Index(len(out))))
208211
if err != nil {
209212
diags = diags.Extend(err)
210213
// Skip the element if it cannot be normalized.
@@ -221,10 +224,10 @@ func (n normalizeOptions) normalizeSlice(typ reflect.Type, src dyn.Value, seen [
221224
return src, diags
222225
}
223226

224-
return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindSequence, src))
227+
return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindSequence, src, path))
225228
}
226229

227-
func (n normalizeOptions) normalizeString(typ reflect.Type, src dyn.Value) (dyn.Value, diag.Diagnostics) {
230+
func (n normalizeOptions) normalizeString(typ reflect.Type, src dyn.Value, path dyn.Path) (dyn.Value, diag.Diagnostics) {
228231
var diags diag.Diagnostics
229232
var out string
230233

@@ -239,15 +242,15 @@ func (n normalizeOptions) normalizeString(typ reflect.Type, src dyn.Value) (dyn.
239242
out = strconv.FormatFloat(src.MustFloat(), 'f', -1, 64)
240243
case dyn.KindNil:
241244
// Return a warning if the field is present but has a null value.
242-
return dyn.InvalidValue, diags.Append(nullWarning(dyn.KindString, src))
245+
return dyn.InvalidValue, diags.Append(nullWarning(dyn.KindString, src, path))
243246
default:
244-
return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindString, src))
247+
return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindString, src, path))
245248
}
246249

247250
return dyn.NewValue(out, src.Location()), diags
248251
}
249252

250-
func (n normalizeOptions) normalizeBool(typ reflect.Type, src dyn.Value) (dyn.Value, diag.Diagnostics) {
253+
func (n normalizeOptions) normalizeBool(typ reflect.Type, src dyn.Value, path dyn.Path) (dyn.Value, diag.Diagnostics) {
251254
var diags diag.Diagnostics
252255
var out bool
253256

@@ -268,19 +271,19 @@ func (n normalizeOptions) normalizeBool(typ reflect.Type, src dyn.Value) (dyn.Va
268271
}
269272

270273
// Cannot interpret as a boolean.
271-
return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindBool, src))
274+
return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindBool, src, path))
272275
}
273276
case dyn.KindNil:
274277
// Return a warning if the field is present but has a null value.
275-
return dyn.InvalidValue, diags.Append(nullWarning(dyn.KindBool, src))
278+
return dyn.InvalidValue, diags.Append(nullWarning(dyn.KindBool, src, path))
276279
default:
277-
return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindBool, src))
280+
return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindBool, src, path))
278281
}
279282

280283
return dyn.NewValue(out, src.Location()), diags
281284
}
282285

283-
func (n normalizeOptions) normalizeInt(typ reflect.Type, src dyn.Value) (dyn.Value, diag.Diagnostics) {
286+
func (n normalizeOptions) normalizeInt(typ reflect.Type, src dyn.Value, path dyn.Path) (dyn.Value, diag.Diagnostics) {
284287
var diags diag.Diagnostics
285288
var out int64
286289

@@ -300,19 +303,20 @@ func (n normalizeOptions) normalizeInt(typ reflect.Type, src dyn.Value) (dyn.Val
300303
Severity: diag.Error,
301304
Summary: fmt.Sprintf("cannot parse %q as an integer", src.MustString()),
302305
Location: src.Location(),
306+
Path: path,
303307
})
304308
}
305309
case dyn.KindNil:
306310
// Return a warning if the field is present but has a null value.
307-
return dyn.InvalidValue, diags.Append(nullWarning(dyn.KindInt, src))
311+
return dyn.InvalidValue, diags.Append(nullWarning(dyn.KindInt, src, path))
308312
default:
309-
return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindInt, src))
313+
return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindInt, src, path))
310314
}
311315

312316
return dyn.NewValue(out, src.Location()), diags
313317
}
314318

315-
func (n normalizeOptions) normalizeFloat(typ reflect.Type, src dyn.Value) (dyn.Value, diag.Diagnostics) {
319+
func (n normalizeOptions) normalizeFloat(typ reflect.Type, src dyn.Value, path dyn.Path) (dyn.Value, diag.Diagnostics) {
316320
var diags diag.Diagnostics
317321
var out float64
318322

@@ -332,13 +336,14 @@ func (n normalizeOptions) normalizeFloat(typ reflect.Type, src dyn.Value) (dyn.V
332336
Severity: diag.Error,
333337
Summary: fmt.Sprintf("cannot parse %q as a floating point number", src.MustString()),
334338
Location: src.Location(),
339+
Path: path,
335340
})
336341
}
337342
case dyn.KindNil:
338343
// Return a warning if the field is present but has a null value.
339-
return dyn.InvalidValue, diags.Append(nullWarning(dyn.KindFloat, src))
344+
return dyn.InvalidValue, diags.Append(nullWarning(dyn.KindFloat, src, path))
340345
default:
341-
return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindFloat, src))
346+
return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindFloat, src, path))
342347
}
343348

344349
return dyn.NewValue(out, src.Location()), diags

libs/dyn/convert/normalize_test.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ func TestNormalizeStructElementDiagnostic(t *testing.T) {
4343
Severity: diag.Error,
4444
Summary: `expected string, found map`,
4545
Location: dyn.Location{},
46+
Path: dyn.NewPath(dyn.Key("bar")),
4647
}, err[0])
4748

4849
// Elements that encounter an error during normalization are dropped.
@@ -68,6 +69,7 @@ func TestNormalizeStructUnknownField(t *testing.T) {
6869
Severity: diag.Warning,
6970
Summary: `unknown field: bar`,
7071
Location: vin.Get("foo").Location(),
72+
Path: dyn.EmptyPath,
7173
}, err[0])
7274

7375
// The field that can be mapped to the struct field is retained.
@@ -101,6 +103,7 @@ func TestNormalizeStructError(t *testing.T) {
101103
Severity: diag.Error,
102104
Summary: `expected map, found string`,
103105
Location: vin.Get("foo").Location(),
106+
Path: dyn.EmptyPath,
104107
}, err[0])
105108
}
106109

@@ -245,6 +248,7 @@ func TestNormalizeMapElementDiagnostic(t *testing.T) {
245248
Severity: diag.Error,
246249
Summary: `expected string, found map`,
247250
Location: dyn.Location{},
251+
Path: dyn.NewPath(dyn.Key("bar")),
248252
}, err[0])
249253

250254
// Elements that encounter an error during normalization are dropped.
@@ -270,6 +274,7 @@ func TestNormalizeMapError(t *testing.T) {
270274
Severity: diag.Error,
271275
Summary: `expected map, found string`,
272276
Location: vin.Location(),
277+
Path: dyn.EmptyPath,
273278
}, err[0])
274279
}
275280

@@ -333,6 +338,7 @@ func TestNormalizeSliceElementDiagnostic(t *testing.T) {
333338
Severity: diag.Error,
334339
Summary: `expected string, found map`,
335340
Location: dyn.Location{},
341+
Path: dyn.NewPath(dyn.Index(2)),
336342
}, err[0])
337343

338344
// Elements that encounter an error during normalization are dropped.
@@ -356,6 +362,7 @@ func TestNormalizeSliceError(t *testing.T) {
356362
Severity: diag.Error,
357363
Summary: `expected sequence, found string`,
358364
Location: vin.Location(),
365+
Path: dyn.EmptyPath,
359366
}, err[0])
360367
}
361368

@@ -410,6 +417,7 @@ func TestNormalizeStringNil(t *testing.T) {
410417
Severity: diag.Warning,
411418
Summary: `expected a string value, found null`,
412419
Location: vin.Location(),
420+
Path: dyn.EmptyPath,
413421
}, err[0])
414422
}
415423

@@ -446,6 +454,7 @@ func TestNormalizeStringError(t *testing.T) {
446454
Severity: diag.Error,
447455
Summary: `expected string, found map`,
448456
Location: dyn.Location{},
457+
Path: dyn.EmptyPath,
449458
}, err[0])
450459
}
451460

@@ -466,6 +475,7 @@ func TestNormalizeBoolNil(t *testing.T) {
466475
Severity: diag.Warning,
467476
Summary: `expected a bool value, found null`,
468477
Location: vin.Location(),
478+
Path: dyn.EmptyPath,
469479
}, err[0])
470480
}
471481

@@ -507,6 +517,7 @@ func TestNormalizeBoolFromStringError(t *testing.T) {
507517
Severity: diag.Error,
508518
Summary: `expected bool, found string`,
509519
Location: vin.Location(),
520+
Path: dyn.EmptyPath,
510521
}, err[0])
511522
}
512523

@@ -519,6 +530,7 @@ func TestNormalizeBoolError(t *testing.T) {
519530
Severity: diag.Error,
520531
Summary: `expected bool, found map`,
521532
Location: dyn.Location{},
533+
Path: dyn.EmptyPath,
522534
}, err[0])
523535
}
524536

@@ -539,6 +551,7 @@ func TestNormalizeIntNil(t *testing.T) {
539551
Severity: diag.Warning,
540552
Summary: `expected a int value, found null`,
541553
Location: vin.Location(),
554+
Path: dyn.EmptyPath,
542555
}, err[0])
543556
}
544557

@@ -567,6 +580,7 @@ func TestNormalizeIntFromStringError(t *testing.T) {
567580
Severity: diag.Error,
568581
Summary: `cannot parse "abc" as an integer`,
569582
Location: vin.Location(),
583+
Path: dyn.EmptyPath,
570584
}, err[0])
571585
}
572586

@@ -579,6 +593,7 @@ func TestNormalizeIntError(t *testing.T) {
579593
Severity: diag.Error,
580594
Summary: `expected int, found map`,
581595
Location: dyn.Location{},
596+
Path: dyn.EmptyPath,
582597
}, err[0])
583598
}
584599

@@ -599,6 +614,7 @@ func TestNormalizeFloatNil(t *testing.T) {
599614
Severity: diag.Warning,
600615
Summary: `expected a float value, found null`,
601616
Location: vin.Location(),
617+
Path: dyn.EmptyPath,
602618
}, err[0])
603619
}
604620

@@ -627,6 +643,7 @@ func TestNormalizeFloatFromStringError(t *testing.T) {
627643
Severity: diag.Error,
628644
Summary: `cannot parse "abc" as a floating point number`,
629645
Location: vin.Location(),
646+
Path: dyn.EmptyPath,
630647
}, err[0])
631648
}
632649

@@ -639,5 +656,6 @@ func TestNormalizeFloatError(t *testing.T) {
639656
Severity: diag.Error,
640657
Summary: `expected float, found map`,
641658
Location: dyn.Location{},
659+
Path: dyn.EmptyPath,
642660
}, err[0])
643661
}

0 commit comments

Comments
 (0)