From 8ed996448206e5870a1d026331a88fd6392c3ede Mon Sep 17 00:00:00 2001 From: shreyas-goenka <88374338+shreyas-goenka@users.noreply.github.com> Date: Tue, 16 Jul 2024 16:57:27 +0530 Subject: [PATCH] Track multiple locations associated with a `dyn.Value` (#1510) ## Changes This PR changes the location metadata associated with a `dyn.Value` to a slice of locations. This will allow us to keep track of location metadata across merges and overrides. The convention is to treat the first location in the slice as the primary location. Also, the semantics are the same as before if there's only one location associated with a value, that is: 1. For complex values (maps, sequences) the location of the v1 is primary in Merge(v1, v2) 2. For primitive values the location of v2 is primary in Merge(v1, v2) ## Tests Modifying existing merge unit tests. Other existing unit tests and integration tests pass. --------- Co-authored-by: Pieter Noordhuis --- bundle/config/generate/job.go | 2 +- .../mutator/expand_pipeline_glob_paths.go | 4 +- .../mutator/python/python_mutator_test.go | 20 +- bundle/config/mutator/rewrite_sync_paths.go | 2 +- bundle/config/mutator/translate_paths.go | 2 +- bundle/config/root.go | 10 +- bundle/internal/bundletest/location.go | 4 +- libs/dyn/convert/from_typed.go | 4 +- libs/dyn/convert/from_typed_test.go | 60 ++--- libs/dyn/convert/normalize.go | 16 +- libs/dyn/convert/normalize_test.go | 54 ++-- libs/dyn/dynvar/resolve.go | 4 +- libs/dyn/merge/elements_by_key.go | 2 +- libs/dyn/merge/merge.go | 35 ++- libs/dyn/merge/merge_test.go | 138 +++++++++-- libs/dyn/merge/override.go | 4 +- libs/dyn/merge/override_test.go | 233 +++++++++--------- libs/dyn/pattern.go | 4 +- libs/dyn/value.go | 48 +++- libs/dyn/value_test.go | 11 +- libs/dyn/value_underlying_test.go | 4 +- libs/dyn/visit_map.go | 4 +- libs/dyn/yamlloader/loader.go | 22 +- libs/dyn/yamlsaver/saver_test.go | 65 ++--- libs/dyn/yamlsaver/utils.go | 2 +- libs/dyn/yamlsaver/utils_test.go | 31 ++- 26 files changed, 472 insertions(+), 313 deletions(-) diff --git a/bundle/config/generate/job.go b/bundle/config/generate/job.go index 3ab5e0122..28bc86412 100644 --- a/bundle/config/generate/job.go +++ b/bundle/config/generate/job.go @@ -22,7 +22,7 @@ func ConvertJobToValue(job *jobs.Job) (dyn.Value, error) { tasks = append(tasks, v) } // We're using location lines to define the order of keys in exported YAML. - value["tasks"] = dyn.NewValue(tasks, dyn.Location{Line: jobOrder.Get("tasks")}) + value["tasks"] = dyn.NewValue(tasks, []dyn.Location{{Line: jobOrder.Get("tasks")}}) } return yamlsaver.ConvertToMapValue(job.Settings, jobOrder, []string{"format", "new_cluster", "existing_cluster_id"}, value) diff --git a/bundle/config/mutator/expand_pipeline_glob_paths.go b/bundle/config/mutator/expand_pipeline_glob_paths.go index 268d8fa48..5703332fa 100644 --- a/bundle/config/mutator/expand_pipeline_glob_paths.go +++ b/bundle/config/mutator/expand_pipeline_glob_paths.go @@ -59,7 +59,7 @@ func (m *expandPipelineGlobPaths) expandLibrary(v dyn.Value) ([]dyn.Value, error if err != nil { return nil, err } - nv, err := dyn.SetByPath(v, p, dyn.NewValue(m, pv.Location())) + nv, err := dyn.SetByPath(v, p, dyn.NewValue(m, pv.Locations())) if err != nil { return nil, err } @@ -90,7 +90,7 @@ func (m *expandPipelineGlobPaths) expandSequence(p dyn.Path, v dyn.Value) (dyn.V vs = append(vs, v...) } - return dyn.NewValue(vs, v.Location()), nil + return dyn.NewValue(vs, v.Locations()), nil } func (m *expandPipelineGlobPaths) Apply(_ context.Context, b *bundle.Bundle) diag.Diagnostics { diff --git a/bundle/config/mutator/python/python_mutator_test.go b/bundle/config/mutator/python/python_mutator_test.go index 9a0ed8c3a..588589831 100644 --- a/bundle/config/mutator/python/python_mutator_test.go +++ b/bundle/config/mutator/python/python_mutator_test.go @@ -305,8 +305,8 @@ type createOverrideVisitorTestCase struct { } func TestCreateOverrideVisitor(t *testing.T) { - left := dyn.NewValue(42, dyn.Location{}) - right := dyn.NewValue(1337, dyn.Location{}) + left := dyn.V(42) + right := dyn.V(1337) testCases := []createOverrideVisitorTestCase{ { @@ -470,21 +470,21 @@ func TestCreateOverrideVisitor_omitempty(t *testing.T) { // this is not happening, but adding for completeness name: "undo delete of empty variables", path: dyn.MustPathFromString("variables"), - left: dyn.NewValue([]dyn.Value{}, location), + left: dyn.NewValue([]dyn.Value{}, []dyn.Location{location}), expectedErr: merge.ErrOverrideUndoDelete, phases: allPhases, }, { name: "undo delete of empty job clusters", path: dyn.MustPathFromString("resources.jobs.job0.job_clusters"), - left: dyn.NewValue([]dyn.Value{}, location), + left: dyn.NewValue([]dyn.Value{}, []dyn.Location{location}), expectedErr: merge.ErrOverrideUndoDelete, phases: allPhases, }, { name: "allow delete of non-empty job clusters", path: dyn.MustPathFromString("resources.jobs.job0.job_clusters"), - left: dyn.NewValue([]dyn.Value{dyn.NewValue("abc", location)}, location), + left: dyn.NewValue([]dyn.Value{dyn.NewValue("abc", []dyn.Location{location})}, []dyn.Location{location}), expectedErr: nil, // deletions aren't allowed in 'load' phase phases: []phase{PythonMutatorPhaseInit}, @@ -492,17 +492,15 @@ func TestCreateOverrideVisitor_omitempty(t *testing.T) { { name: "undo delete of empty tags", path: dyn.MustPathFromString("resources.jobs.job0.tags"), - left: dyn.NewValue(map[string]dyn.Value{}, location), + left: dyn.NewValue(map[string]dyn.Value{}, []dyn.Location{location}), expectedErr: merge.ErrOverrideUndoDelete, phases: allPhases, }, { name: "allow delete of non-empty tags", path: dyn.MustPathFromString("resources.jobs.job0.tags"), - left: dyn.NewValue( - map[string]dyn.Value{"dev": dyn.NewValue("true", location)}, - location, - ), + left: dyn.NewValue(map[string]dyn.Value{"dev": dyn.NewValue("true", []dyn.Location{location})}, []dyn.Location{location}), + expectedErr: nil, // deletions aren't allowed in 'load' phase phases: []phase{PythonMutatorPhaseInit}, @@ -510,7 +508,7 @@ func TestCreateOverrideVisitor_omitempty(t *testing.T) { { name: "undo delete of nil", path: dyn.MustPathFromString("resources.jobs.job0.tags"), - left: dyn.NilValue.WithLocation(location), + left: dyn.NilValue.WithLocations([]dyn.Location{location}), expectedErr: merge.ErrOverrideUndoDelete, phases: allPhases, }, diff --git a/bundle/config/mutator/rewrite_sync_paths.go b/bundle/config/mutator/rewrite_sync_paths.go index 85db79797..cfdc55f36 100644 --- a/bundle/config/mutator/rewrite_sync_paths.go +++ b/bundle/config/mutator/rewrite_sync_paths.go @@ -38,7 +38,7 @@ func (m *rewriteSyncPaths) makeRelativeTo(root string) dyn.MapFunc { return dyn.InvalidValue, err } - return dyn.NewValue(filepath.Join(rel, v.MustString()), v.Location()), nil + return dyn.NewValue(filepath.Join(rel, v.MustString()), v.Locations()), nil } } diff --git a/bundle/config/mutator/translate_paths.go b/bundle/config/mutator/translate_paths.go index a01d3d6a7..28f7d3d30 100644 --- a/bundle/config/mutator/translate_paths.go +++ b/bundle/config/mutator/translate_paths.go @@ -182,7 +182,7 @@ func (t *translateContext) rewriteValue(p dyn.Path, v dyn.Value, fn rewriteFunc, return dyn.InvalidValue, err } - return dyn.NewValue(out, v.Location()), nil + return dyn.NewValue(out, v.Locations()), nil } func (t *translateContext) rewriteRelativeTo(p dyn.Path, v dyn.Value, fn rewriteFunc, dir, fallback string) (dyn.Value, error) { diff --git a/bundle/config/root.go b/bundle/config/root.go index 2bbb78696..594a9105f 100644 --- a/bundle/config/root.go +++ b/bundle/config/root.go @@ -378,7 +378,7 @@ func (r *Root) MergeTargetOverrides(name string) error { // Below, we're setting fields on the bundle key, so make sure it exists. if root.Get("bundle").Kind() == dyn.KindInvalid { - root, err = dyn.Set(root, "bundle", dyn.NewValue(map[string]dyn.Value{}, dyn.Location{})) + root, err = dyn.Set(root, "bundle", dyn.V(map[string]dyn.Value{})) if err != nil { return err } @@ -404,7 +404,7 @@ func (r *Root) MergeTargetOverrides(name string) error { if v := target.Get("git"); v.Kind() != dyn.KindInvalid { ref, err := dyn.GetByPath(root, dyn.NewPath(dyn.Key("bundle"), dyn.Key("git"))) if err != nil { - ref = dyn.NewValue(map[string]dyn.Value{}, dyn.Location{}) + ref = dyn.V(map[string]dyn.Value{}) } // Merge the override into the reference. @@ -415,7 +415,7 @@ func (r *Root) MergeTargetOverrides(name string) error { // If the branch was overridden, we need to clear the inferred flag. if branch := v.Get("branch"); branch.Kind() != dyn.KindInvalid { - out, err = dyn.SetByPath(out, dyn.NewPath(dyn.Key("inferred")), dyn.NewValue(false, dyn.Location{})) + out, err = dyn.SetByPath(out, dyn.NewPath(dyn.Key("inferred")), dyn.V(false)) if err != nil { return err } @@ -456,7 +456,7 @@ func rewriteShorthands(v dyn.Value) (dyn.Value, error) { // configuration will convert this to a string if necessary. return dyn.NewValue(map[string]dyn.Value{ "default": variable, - }, variable.Location()), nil + }, variable.Locations()), nil case dyn.KindMap, dyn.KindSequence: // Check if the original definition of variable has a type field. @@ -469,7 +469,7 @@ func rewriteShorthands(v dyn.Value) (dyn.Value, error) { return dyn.NewValue(map[string]dyn.Value{ "type": typeV, "default": variable, - }, variable.Location()), nil + }, variable.Locations()), nil } return variable, nil diff --git a/bundle/internal/bundletest/location.go b/bundle/internal/bundletest/location.go index 1fd6f968c..ebec43d30 100644 --- a/bundle/internal/bundletest/location.go +++ b/bundle/internal/bundletest/location.go @@ -14,9 +14,9 @@ func SetLocation(b *bundle.Bundle, prefix string, filePath string) { return dyn.Walk(root, func(p dyn.Path, v dyn.Value) (dyn.Value, error) { // If the path has the given prefix, set the location. if p.HasPrefix(start) { - return v.WithLocation(dyn.Location{ + return v.WithLocations([]dyn.Location{{ File: filePath, - }), nil + }}), nil } // The path is not nested under the given prefix. diff --git a/libs/dyn/convert/from_typed.go b/libs/dyn/convert/from_typed.go index e8d321f66..cd92ad0eb 100644 --- a/libs/dyn/convert/from_typed.go +++ b/libs/dyn/convert/from_typed.go @@ -42,7 +42,7 @@ func fromTyped(src any, ref dyn.Value, options ...fromTypedOptions) (dyn.Value, // Dereference pointer if necessary for srcv.Kind() == reflect.Pointer { if srcv.IsNil() { - return dyn.NilValue.WithLocation(ref.Location()), nil + return dyn.NilValue.WithLocations(ref.Locations()), nil } srcv = srcv.Elem() @@ -83,7 +83,7 @@ func fromTyped(src any, ref dyn.Value, options ...fromTypedOptions) (dyn.Value, if err != nil { return dyn.InvalidValue, err } - return v.WithLocation(ref.Location()), err + return v.WithLocations(ref.Locations()), err } func fromTypedStruct(src reflect.Value, ref dyn.Value, options ...fromTypedOptions) (dyn.Value, error) { diff --git a/libs/dyn/convert/from_typed_test.go b/libs/dyn/convert/from_typed_test.go index 9141a6948..0cddff3be 100644 --- a/libs/dyn/convert/from_typed_test.go +++ b/libs/dyn/convert/from_typed_test.go @@ -115,16 +115,16 @@ func TestFromTypedStructSetFieldsRetainLocation(t *testing.T) { } ref := dyn.V(map[string]dyn.Value{ - "foo": dyn.NewValue("bar", dyn.Location{File: "foo"}), - "bar": dyn.NewValue("baz", dyn.Location{File: "bar"}), + "foo": dyn.NewValue("bar", []dyn.Location{{File: "foo"}}), + "bar": dyn.NewValue("baz", []dyn.Location{{File: "bar"}}), }) nv, err := FromTyped(src, ref) require.NoError(t, err) // Assert foo and bar have retained their location. - assert.Equal(t, dyn.NewValue("bar", dyn.Location{File: "foo"}), nv.Get("foo")) - assert.Equal(t, dyn.NewValue("qux", dyn.Location{File: "bar"}), nv.Get("bar")) + assert.Equal(t, dyn.NewValue("bar", []dyn.Location{{File: "foo"}}), nv.Get("foo")) + assert.Equal(t, dyn.NewValue("qux", []dyn.Location{{File: "bar"}}), nv.Get("bar")) } func TestFromTypedStringMapWithZeroValue(t *testing.T) { @@ -359,16 +359,16 @@ func TestFromTypedMapNonEmptyRetainLocation(t *testing.T) { } ref := dyn.V(map[string]dyn.Value{ - "foo": dyn.NewValue("bar", dyn.Location{File: "foo"}), - "bar": dyn.NewValue("baz", dyn.Location{File: "bar"}), + "foo": dyn.NewValue("bar", []dyn.Location{{File: "foo"}}), + "bar": dyn.NewValue("baz", []dyn.Location{{File: "bar"}}), }) nv, err := FromTyped(src, ref) require.NoError(t, err) // Assert foo and bar have retained their locations. - assert.Equal(t, dyn.NewValue("bar", dyn.Location{File: "foo"}), nv.Get("foo")) - assert.Equal(t, dyn.NewValue("qux", dyn.Location{File: "bar"}), nv.Get("bar")) + assert.Equal(t, dyn.NewValue("bar", []dyn.Location{{File: "foo"}}), nv.Get("foo")) + assert.Equal(t, dyn.NewValue("qux", []dyn.Location{{File: "bar"}}), nv.Get("bar")) } func TestFromTypedMapFieldWithZeroValue(t *testing.T) { @@ -432,16 +432,16 @@ func TestFromTypedSliceNonEmptyRetainLocation(t *testing.T) { } ref := dyn.V([]dyn.Value{ - dyn.NewValue("foo", dyn.Location{File: "foo"}), - dyn.NewValue("bar", dyn.Location{File: "bar"}), + dyn.NewValue("foo", []dyn.Location{{File: "foo"}}), + dyn.NewValue("bar", []dyn.Location{{File: "bar"}}), }) nv, err := FromTyped(src, ref) require.NoError(t, err) // Assert foo and bar have retained their locations. - assert.Equal(t, dyn.NewValue("foo", dyn.Location{File: "foo"}), nv.Index(0)) - assert.Equal(t, dyn.NewValue("bar", dyn.Location{File: "bar"}), nv.Index(1)) + assert.Equal(t, dyn.NewValue("foo", []dyn.Location{{File: "foo"}}), nv.Index(0)) + assert.Equal(t, dyn.NewValue("bar", []dyn.Location{{File: "bar"}}), nv.Index(1)) } func TestFromTypedStringEmpty(t *testing.T) { @@ -477,19 +477,19 @@ func TestFromTypedStringNonEmptyOverwrite(t *testing.T) { } func TestFromTypedStringRetainsLocations(t *testing.T) { - var ref = dyn.NewValue("foo", dyn.Location{File: "foo"}) + var ref = dyn.NewValue("foo", []dyn.Location{{File: "foo"}}) // case: value has not been changed var src string = "foo" nv, err := FromTyped(src, ref) require.NoError(t, err) - assert.Equal(t, dyn.NewValue("foo", dyn.Location{File: "foo"}), nv) + assert.Equal(t, dyn.NewValue("foo", []dyn.Location{{File: "foo"}}), nv) // case: value has been changed src = "bar" nv, err = FromTyped(src, ref) require.NoError(t, err) - assert.Equal(t, dyn.NewValue("bar", dyn.Location{File: "foo"}), nv) + assert.Equal(t, dyn.NewValue("bar", []dyn.Location{{File: "foo"}}), nv) } func TestFromTypedStringTypeError(t *testing.T) { @@ -532,19 +532,19 @@ func TestFromTypedBoolNonEmptyOverwrite(t *testing.T) { } func TestFromTypedBoolRetainsLocations(t *testing.T) { - var ref = dyn.NewValue(true, dyn.Location{File: "foo"}) + var ref = dyn.NewValue(true, []dyn.Location{{File: "foo"}}) // case: value has not been changed var src bool = true nv, err := FromTyped(src, ref) require.NoError(t, err) - assert.Equal(t, dyn.NewValue(true, dyn.Location{File: "foo"}), nv) + assert.Equal(t, dyn.NewValue(true, []dyn.Location{{File: "foo"}}), nv) // case: value has been changed src = false nv, err = FromTyped(src, ref) require.NoError(t, err) - assert.Equal(t, dyn.NewValue(false, dyn.Location{File: "foo"}), nv) + assert.Equal(t, dyn.NewValue(false, []dyn.Location{{File: "foo"}}), nv) } func TestFromTypedBoolVariableReference(t *testing.T) { @@ -595,19 +595,19 @@ func TestFromTypedIntNonEmptyOverwrite(t *testing.T) { } func TestFromTypedIntRetainsLocations(t *testing.T) { - var ref = dyn.NewValue(1234, dyn.Location{File: "foo"}) + var ref = dyn.NewValue(1234, []dyn.Location{{File: "foo"}}) // case: value has not been changed var src int = 1234 nv, err := FromTyped(src, ref) require.NoError(t, err) - assert.Equal(t, dyn.NewValue(1234, dyn.Location{File: "foo"}), nv) + assert.Equal(t, dyn.NewValue(1234, []dyn.Location{{File: "foo"}}), nv) // case: value has been changed src = 1235 nv, err = FromTyped(src, ref) require.NoError(t, err) - assert.Equal(t, dyn.NewValue(int64(1235), dyn.Location{File: "foo"}), nv) + assert.Equal(t, dyn.NewValue(int64(1235), []dyn.Location{{File: "foo"}}), nv) } func TestFromTypedIntVariableReference(t *testing.T) { @@ -659,19 +659,19 @@ func TestFromTypedFloatNonEmptyOverwrite(t *testing.T) { func TestFromTypedFloatRetainsLocations(t *testing.T) { var src float64 - var ref = dyn.NewValue(1.23, dyn.Location{File: "foo"}) + var ref = dyn.NewValue(1.23, []dyn.Location{{File: "foo"}}) // case: value has not been changed src = 1.23 nv, err := FromTyped(src, ref) require.NoError(t, err) - assert.Equal(t, dyn.NewValue(1.23, dyn.Location{File: "foo"}), nv) + assert.Equal(t, dyn.NewValue(1.23, []dyn.Location{{File: "foo"}}), nv) // case: value has been changed src = 1.24 nv, err = FromTyped(src, ref) require.NoError(t, err) - assert.Equal(t, dyn.NewValue(1.24, dyn.Location{File: "foo"}), nv) + assert.Equal(t, dyn.NewValue(1.24, []dyn.Location{{File: "foo"}}), nv) } func TestFromTypedFloatVariableReference(t *testing.T) { @@ -740,27 +740,27 @@ func TestFromTypedNilPointerRetainsLocations(t *testing.T) { } var src *Tmp - ref := dyn.NewValue(nil, dyn.Location{File: "foobar"}) + ref := dyn.NewValue(nil, []dyn.Location{{File: "foobar"}}) nv, err := FromTyped(src, ref) require.NoError(t, err) - assert.Equal(t, dyn.NewValue(nil, dyn.Location{File: "foobar"}), nv) + assert.Equal(t, dyn.NewValue(nil, []dyn.Location{{File: "foobar"}}), nv) } func TestFromTypedNilMapRetainsLocation(t *testing.T) { var src map[string]string - ref := dyn.NewValue(nil, dyn.Location{File: "foobar"}) + ref := dyn.NewValue(nil, []dyn.Location{{File: "foobar"}}) nv, err := FromTyped(src, ref) require.NoError(t, err) - assert.Equal(t, dyn.NewValue(nil, dyn.Location{File: "foobar"}), nv) + assert.Equal(t, dyn.NewValue(nil, []dyn.Location{{File: "foobar"}}), nv) } func TestFromTypedNilSliceRetainsLocation(t *testing.T) { var src []string - ref := dyn.NewValue(nil, dyn.Location{File: "foobar"}) + ref := dyn.NewValue(nil, []dyn.Location{{File: "foobar"}}) nv, err := FromTyped(src, ref) require.NoError(t, err) - assert.Equal(t, dyn.NewValue(nil, dyn.Location{File: "foobar"}), nv) + assert.Equal(t, dyn.NewValue(nil, []dyn.Location{{File: "foobar"}}), nv) } diff --git a/libs/dyn/convert/normalize.go b/libs/dyn/convert/normalize.go index ad82e20ef..246c97eaf 100644 --- a/libs/dyn/convert/normalize.go +++ b/libs/dyn/convert/normalize.go @@ -120,7 +120,7 @@ func (n normalizeOptions) normalizeStruct(typ reflect.Type, src dyn.Value, seen // Return the normalized value if missing fields are not included. if !n.includeMissingFields { - return dyn.NewValue(out, src.Location()), diags + return dyn.NewValue(out, src.Locations()), diags } // Populate missing fields with their zero values. @@ -165,7 +165,7 @@ func (n normalizeOptions) normalizeStruct(typ reflect.Type, src dyn.Value, seen } } - return dyn.NewValue(out, src.Location()), diags + return dyn.NewValue(out, src.Locations()), diags case dyn.KindNil: return src, diags @@ -203,7 +203,7 @@ func (n normalizeOptions) normalizeMap(typ reflect.Type, src dyn.Value, seen []r out.Set(pk, nv) } - return dyn.NewValue(out, src.Location()), diags + return dyn.NewValue(out, src.Locations()), diags case dyn.KindNil: return src, diags @@ -238,7 +238,7 @@ func (n normalizeOptions) normalizeSlice(typ reflect.Type, src dyn.Value, seen [ out = append(out, v) } - return dyn.NewValue(out, src.Location()), diags + return dyn.NewValue(out, src.Locations()), diags case dyn.KindNil: return src, diags @@ -273,7 +273,7 @@ func (n normalizeOptions) normalizeString(typ reflect.Type, src dyn.Value, path return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindString, src, path)) } - return dyn.NewValue(out, src.Location()), diags + return dyn.NewValue(out, src.Locations()), diags } func (n normalizeOptions) normalizeBool(typ reflect.Type, src dyn.Value, path dyn.Path) (dyn.Value, diag.Diagnostics) { @@ -306,7 +306,7 @@ func (n normalizeOptions) normalizeBool(typ reflect.Type, src dyn.Value, path dy return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindBool, src, path)) } - return dyn.NewValue(out, src.Location()), diags + return dyn.NewValue(out, src.Locations()), diags } func (n normalizeOptions) normalizeInt(typ reflect.Type, src dyn.Value, path dyn.Path) (dyn.Value, diag.Diagnostics) { @@ -349,7 +349,7 @@ func (n normalizeOptions) normalizeInt(typ reflect.Type, src dyn.Value, path dyn return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindInt, src, path)) } - return dyn.NewValue(out, src.Location()), diags + return dyn.NewValue(out, src.Locations()), diags } func (n normalizeOptions) normalizeFloat(typ reflect.Type, src dyn.Value, path dyn.Path) (dyn.Value, diag.Diagnostics) { @@ -392,7 +392,7 @@ func (n normalizeOptions) normalizeFloat(typ reflect.Type, src dyn.Value, path d return dyn.InvalidValue, diags.Append(typeMismatch(dyn.KindFloat, src, path)) } - return dyn.NewValue(out, src.Location()), diags + return dyn.NewValue(out, src.Locations()), diags } func (n normalizeOptions) normalizeInterface(typ reflect.Type, src dyn.Value, path dyn.Path) (dyn.Value, diag.Diagnostics) { diff --git a/libs/dyn/convert/normalize_test.go b/libs/dyn/convert/normalize_test.go index 299ffcabd..452ed4eb1 100644 --- a/libs/dyn/convert/normalize_test.go +++ b/libs/dyn/convert/normalize_test.go @@ -229,7 +229,7 @@ func TestNormalizeStructVariableReference(t *testing.T) { } var typ Tmp - vin := dyn.NewValue("${var.foo}", dyn.Location{File: "file", Line: 1, Column: 1}) + vin := dyn.NewValue("${var.foo}", []dyn.Location{{File: "file", Line: 1, Column: 1}}) vout, err := Normalize(typ, vin) assert.Empty(t, err) assert.Equal(t, vin, vout) @@ -241,7 +241,7 @@ func TestNormalizeStructRandomStringError(t *testing.T) { } var typ Tmp - vin := dyn.NewValue("var foo", dyn.Location{File: "file", Line: 1, Column: 1}) + vin := dyn.NewValue("var foo", []dyn.Location{{File: "file", Line: 1, Column: 1}}) _, err := Normalize(typ, vin) assert.Len(t, err, 1) assert.Equal(t, diag.Diagnostic{ @@ -258,7 +258,7 @@ func TestNormalizeStructIntError(t *testing.T) { } var typ Tmp - vin := dyn.NewValue(1, dyn.Location{File: "file", Line: 1, Column: 1}) + vin := dyn.NewValue(1, []dyn.Location{{File: "file", Line: 1, Column: 1}}) _, err := Normalize(typ, vin) assert.Len(t, err, 1) assert.Equal(t, diag.Diagnostic{ @@ -360,7 +360,7 @@ func TestNormalizeMapNestedError(t *testing.T) { func TestNormalizeMapVariableReference(t *testing.T) { var typ map[string]string - vin := dyn.NewValue("${var.foo}", dyn.Location{File: "file", Line: 1, Column: 1}) + vin := dyn.NewValue("${var.foo}", []dyn.Location{{File: "file", Line: 1, Column: 1}}) vout, err := Normalize(typ, vin) assert.Empty(t, err) assert.Equal(t, vin, vout) @@ -368,7 +368,7 @@ func TestNormalizeMapVariableReference(t *testing.T) { func TestNormalizeMapRandomStringError(t *testing.T) { var typ map[string]string - vin := dyn.NewValue("var foo", dyn.Location{File: "file", Line: 1, Column: 1}) + vin := dyn.NewValue("var foo", []dyn.Location{{File: "file", Line: 1, Column: 1}}) _, err := Normalize(typ, vin) assert.Len(t, err, 1) assert.Equal(t, diag.Diagnostic{ @@ -381,7 +381,7 @@ func TestNormalizeMapRandomStringError(t *testing.T) { func TestNormalizeMapIntError(t *testing.T) { var typ map[string]string - vin := dyn.NewValue(1, dyn.Location{File: "file", Line: 1, Column: 1}) + vin := dyn.NewValue(1, []dyn.Location{{File: "file", Line: 1, Column: 1}}) _, err := Normalize(typ, vin) assert.Len(t, err, 1) assert.Equal(t, diag.Diagnostic{ @@ -482,7 +482,7 @@ func TestNormalizeSliceNestedError(t *testing.T) { func TestNormalizeSliceVariableReference(t *testing.T) { var typ []string - vin := dyn.NewValue("${var.foo}", dyn.Location{File: "file", Line: 1, Column: 1}) + vin := dyn.NewValue("${var.foo}", []dyn.Location{{File: "file", Line: 1, Column: 1}}) vout, err := Normalize(typ, vin) assert.Empty(t, err) assert.Equal(t, vin, vout) @@ -490,7 +490,7 @@ func TestNormalizeSliceVariableReference(t *testing.T) { func TestNormalizeSliceRandomStringError(t *testing.T) { var typ []string - vin := dyn.NewValue("var foo", dyn.Location{File: "file", Line: 1, Column: 1}) + vin := dyn.NewValue("var foo", []dyn.Location{{File: "file", Line: 1, Column: 1}}) _, err := Normalize(typ, vin) assert.Len(t, err, 1) assert.Equal(t, diag.Diagnostic{ @@ -503,7 +503,7 @@ func TestNormalizeSliceRandomStringError(t *testing.T) { func TestNormalizeSliceIntError(t *testing.T) { var typ []string - vin := dyn.NewValue(1, dyn.Location{File: "file", Line: 1, Column: 1}) + vin := dyn.NewValue(1, []dyn.Location{{File: "file", Line: 1, Column: 1}}) _, err := Normalize(typ, vin) assert.Len(t, err, 1) assert.Equal(t, diag.Diagnostic{ @@ -524,7 +524,7 @@ func TestNormalizeString(t *testing.T) { func TestNormalizeStringNil(t *testing.T) { var typ string - vin := dyn.NewValue(nil, dyn.Location{File: "file", Line: 1, Column: 1}) + vin := dyn.NewValue(nil, []dyn.Location{{File: "file", Line: 1, Column: 1}}) _, err := Normalize(&typ, vin) assert.Len(t, err, 1) assert.Equal(t, diag.Diagnostic{ @@ -537,26 +537,26 @@ func TestNormalizeStringNil(t *testing.T) { func TestNormalizeStringFromBool(t *testing.T) { var typ string - vin := dyn.NewValue(true, dyn.Location{File: "file", Line: 1, Column: 1}) + vin := dyn.NewValue(true, []dyn.Location{{File: "file", Line: 1, Column: 1}}) vout, err := Normalize(&typ, vin) assert.Empty(t, err) - assert.Equal(t, dyn.NewValue("true", vin.Location()), vout) + assert.Equal(t, dyn.NewValue("true", vin.Locations()), vout) } func TestNormalizeStringFromInt(t *testing.T) { var typ string - vin := dyn.NewValue(123, dyn.Location{File: "file", Line: 1, Column: 1}) + vin := dyn.NewValue(123, []dyn.Location{{File: "file", Line: 1, Column: 1}}) vout, err := Normalize(&typ, vin) assert.Empty(t, err) - assert.Equal(t, dyn.NewValue("123", vin.Location()), vout) + assert.Equal(t, dyn.NewValue("123", vin.Locations()), vout) } func TestNormalizeStringFromFloat(t *testing.T) { var typ string - vin := dyn.NewValue(1.20, dyn.Location{File: "file", Line: 1, Column: 1}) + vin := dyn.NewValue(1.20, []dyn.Location{{File: "file", Line: 1, Column: 1}}) vout, err := Normalize(&typ, vin) assert.Empty(t, err) - assert.Equal(t, dyn.NewValue("1.2", vin.Location()), vout) + assert.Equal(t, dyn.NewValue("1.2", vin.Locations()), vout) } func TestNormalizeStringError(t *testing.T) { @@ -582,7 +582,7 @@ func TestNormalizeBool(t *testing.T) { func TestNormalizeBoolNil(t *testing.T) { var typ bool - vin := dyn.NewValue(nil, dyn.Location{File: "file", Line: 1, Column: 1}) + vin := dyn.NewValue(nil, []dyn.Location{{File: "file", Line: 1, Column: 1}}) _, err := Normalize(&typ, vin) assert.Len(t, err, 1) assert.Equal(t, diag.Diagnostic{ @@ -658,7 +658,7 @@ func TestNormalizeInt(t *testing.T) { func TestNormalizeIntNil(t *testing.T) { var typ int - vin := dyn.NewValue(nil, dyn.Location{File: "file", Line: 1, Column: 1}) + vin := dyn.NewValue(nil, []dyn.Location{{File: "file", Line: 1, Column: 1}}) _, err := Normalize(&typ, vin) assert.Len(t, err, 1) assert.Equal(t, diag.Diagnostic{ @@ -742,7 +742,7 @@ func TestNormalizeFloat(t *testing.T) { func TestNormalizeFloatNil(t *testing.T) { var typ float64 - vin := dyn.NewValue(nil, dyn.Location{File: "file", Line: 1, Column: 1}) + vin := dyn.NewValue(nil, []dyn.Location{{File: "file", Line: 1, Column: 1}}) _, err := Normalize(&typ, vin) assert.Len(t, err, 1) assert.Equal(t, diag.Diagnostic{ @@ -842,26 +842,26 @@ func TestNormalizeAnchors(t *testing.T) { func TestNormalizeBoolToAny(t *testing.T) { var typ any - vin := dyn.NewValue(false, dyn.Location{File: "file", Line: 1, Column: 1}) + vin := dyn.NewValue(false, []dyn.Location{{File: "file", Line: 1, Column: 1}}) vout, err := Normalize(&typ, vin) assert.Len(t, err, 0) - assert.Equal(t, dyn.NewValue(false, dyn.Location{File: "file", Line: 1, Column: 1}), vout) + assert.Equal(t, dyn.NewValue(false, []dyn.Location{{File: "file", Line: 1, Column: 1}}), vout) } func TestNormalizeIntToAny(t *testing.T) { var typ any - vin := dyn.NewValue(10, dyn.Location{File: "file", Line: 1, Column: 1}) + vin := dyn.NewValue(10, []dyn.Location{{File: "file", Line: 1, Column: 1}}) vout, err := Normalize(&typ, vin) assert.Len(t, err, 0) - assert.Equal(t, dyn.NewValue(10, dyn.Location{File: "file", Line: 1, Column: 1}), vout) + assert.Equal(t, dyn.NewValue(10, []dyn.Location{{File: "file", Line: 1, Column: 1}}), vout) } func TestNormalizeSliceToAny(t *testing.T) { var typ any - v1 := dyn.NewValue(1, dyn.Location{File: "file", Line: 1, Column: 1}) - v2 := dyn.NewValue(2, dyn.Location{File: "file", Line: 1, Column: 1}) - vin := dyn.NewValue([]dyn.Value{v1, v2}, dyn.Location{File: "file", Line: 1, Column: 1}) + v1 := dyn.NewValue(1, []dyn.Location{{File: "file", Line: 1, Column: 1}}) + v2 := dyn.NewValue(2, []dyn.Location{{File: "file", Line: 1, Column: 1}}) + vin := dyn.NewValue([]dyn.Value{v1, v2}, []dyn.Location{{File: "file", Line: 1, Column: 1}}) vout, err := Normalize(&typ, vin) assert.Len(t, err, 0) - assert.Equal(t, dyn.NewValue([]dyn.Value{v1, v2}, dyn.Location{File: "file", Line: 1, Column: 1}), vout) + assert.Equal(t, dyn.NewValue([]dyn.Value{v1, v2}, []dyn.Location{{File: "file", Line: 1, Column: 1}}), vout) } diff --git a/libs/dyn/dynvar/resolve.go b/libs/dyn/dynvar/resolve.go index d2494bc21..111da25c8 100644 --- a/libs/dyn/dynvar/resolve.go +++ b/libs/dyn/dynvar/resolve.go @@ -155,7 +155,7 @@ func (r *resolver) resolveRef(ref ref, seen []string) (dyn.Value, error) { // of where it is used. This also means that relative path resolution is done // relative to where a variable is used, not where it is defined. // - return dyn.NewValue(resolved[0].Value(), ref.value.Location()), nil + return dyn.NewValue(resolved[0].Value(), ref.value.Locations()), nil } // Not pure; perform string interpolation. @@ -178,7 +178,7 @@ func (r *resolver) resolveRef(ref ref, seen []string) (dyn.Value, error) { ref.str = strings.Replace(ref.str, ref.matches[j][0], s, 1) } - return dyn.NewValue(ref.str, ref.value.Location()), nil + return dyn.NewValue(ref.str, ref.value.Locations()), nil } func (r *resolver) resolveKey(key string, seen []string) (dyn.Value, error) { diff --git a/libs/dyn/merge/elements_by_key.go b/libs/dyn/merge/elements_by_key.go index da20ee849..e6e640d14 100644 --- a/libs/dyn/merge/elements_by_key.go +++ b/libs/dyn/merge/elements_by_key.go @@ -52,7 +52,7 @@ func (e elementsByKey) Map(_ dyn.Path, v dyn.Value) (dyn.Value, error) { out = append(out, nv) } - return dyn.NewValue(out, v.Location()), nil + return dyn.NewValue(out, v.Locations()), nil } // ElementsByKey returns a [dyn.MapFunc] that operates on a sequence diff --git a/libs/dyn/merge/merge.go b/libs/dyn/merge/merge.go index ffe000da3..29decd779 100644 --- a/libs/dyn/merge/merge.go +++ b/libs/dyn/merge/merge.go @@ -12,6 +12,26 @@ import ( // * Merging x with nil or nil with x always yields x. // * Merging maps a and b means entries from map b take precedence. // * Merging sequences a and b means concatenating them. +// +// Merging retains and accumulates the locations metadata associated with the values. +// This allows users of the module to track the provenance of values across merging of +// configuration trees, which is useful for reporting errors and warnings. +// +// Semantics for location metadata in the merged value are similar to the semantics +// for the values themselves: +// +// - When merging x with nil or nil with x, the location of x is retained. +// +// - When merging maps or sequences, the combined value retains the location of a and +// accumulates the location of b. The individual elements of the map or sequence retain +// their original locations, i.e., whether they were originally defined in a or b. +// +// The rationale for retaining location of a is that we would like to return +// the first location a bit of configuration showed up when reporting errors and warnings. +// +// - Merging primitive values means using the incoming value `b`. The location of the +// incoming value is retained and the location of the existing value `a` is accumulated. +// This is because the incoming value overwrites the existing value. func Merge(a, b dyn.Value) (dyn.Value, error) { return merge(a, b) } @@ -22,12 +42,12 @@ func merge(a, b dyn.Value) (dyn.Value, error) { // If a is nil, return b. if ak == dyn.KindNil { - return b, nil + return b.AppendLocationsFromValue(a), nil } // If b is nil, return a. if bk == dyn.KindNil { - return a, nil + return a.AppendLocationsFromValue(b), nil } // Call the appropriate merge function based on the kind of a and b. @@ -75,8 +95,8 @@ func mergeMap(a, b dyn.Value) (dyn.Value, error) { } } - // Preserve the location of the first value. - return dyn.NewValue(out, a.Location()), nil + // Preserve the location of the first value. Accumulate the locations of the second value. + return dyn.NewValue(out, a.Locations()).AppendLocationsFromValue(b), nil } func mergeSequence(a, b dyn.Value) (dyn.Value, error) { @@ -88,11 +108,10 @@ func mergeSequence(a, b dyn.Value) (dyn.Value, error) { copy(out[:], as) copy(out[len(as):], bs) - // Preserve the location of the first value. - return dyn.NewValue(out, a.Location()), nil + // Preserve the location of the first value. Accumulate the locations of the second value. + return dyn.NewValue(out, a.Locations()).AppendLocationsFromValue(b), nil } - func mergePrimitive(a, b dyn.Value) (dyn.Value, error) { // Merging primitive values means using the incoming value. - return b, nil + return b.AppendLocationsFromValue(a), nil } diff --git a/libs/dyn/merge/merge_test.go b/libs/dyn/merge/merge_test.go index 3706dbd77..4a4bf9e6c 100644 --- a/libs/dyn/merge/merge_test.go +++ b/libs/dyn/merge/merge_test.go @@ -8,15 +8,17 @@ import ( ) func TestMergeMaps(t *testing.T) { - v1 := dyn.V(map[string]dyn.Value{ - "foo": dyn.V("bar"), - "bar": dyn.V("baz"), - }) + l1 := dyn.Location{File: "file1", Line: 1, Column: 2} + v1 := dyn.NewValue(map[string]dyn.Value{ + "foo": dyn.NewValue("bar", []dyn.Location{l1}), + "bar": dyn.NewValue("baz", []dyn.Location{l1}), + }, []dyn.Location{l1}) - v2 := dyn.V(map[string]dyn.Value{ - "bar": dyn.V("qux"), - "qux": dyn.V("foo"), - }) + l2 := dyn.Location{File: "file2", Line: 3, Column: 4} + v2 := dyn.NewValue(map[string]dyn.Value{ + "bar": dyn.NewValue("qux", []dyn.Location{l2}), + "qux": dyn.NewValue("foo", []dyn.Location{l2}), + }, []dyn.Location{l2}) // Merge v2 into v1. { @@ -27,6 +29,23 @@ func TestMergeMaps(t *testing.T) { "bar": "qux", "qux": "foo", }, out.AsAny()) + + // Locations of both values should be preserved. + assert.Equal(t, []dyn.Location{l1, l2}, out.Locations()) + assert.Equal(t, []dyn.Location{l2, l1}, out.Get("bar").Locations()) + assert.Equal(t, []dyn.Location{l1}, out.Get("foo").Locations()) + assert.Equal(t, []dyn.Location{l2}, out.Get("qux").Locations()) + + // Location of the merged value should be the location of v1. + assert.Equal(t, l1, out.Location()) + + // Value of bar is "qux" which comes from v2. This .Location() should + // return the location of v2. + assert.Equal(t, l2, out.Get("bar").Location()) + + // Original locations of keys that were not overwritten should be preserved. + assert.Equal(t, l1, out.Get("foo").Location()) + assert.Equal(t, l2, out.Get("qux").Location()) } // Merge v1 into v2. @@ -38,30 +57,64 @@ func TestMergeMaps(t *testing.T) { "bar": "baz", "qux": "foo", }, out.AsAny()) + + // Locations of both values should be preserved. + assert.Equal(t, []dyn.Location{l2, l1}, out.Locations()) + assert.Equal(t, []dyn.Location{l1, l2}, out.Get("bar").Locations()) + assert.Equal(t, []dyn.Location{l1}, out.Get("foo").Locations()) + assert.Equal(t, []dyn.Location{l2}, out.Get("qux").Locations()) + + // Location of the merged value should be the location of v2. + assert.Equal(t, l2, out.Location()) + + // Value of bar is "baz" which comes from v1. This .Location() should + // return the location of v1. + assert.Equal(t, l1, out.Get("bar").Location()) + + // Original locations of keys that were not overwritten should be preserved. + assert.Equal(t, l1, out.Get("foo").Location()) + assert.Equal(t, l2, out.Get("qux").Location()) } + } func TestMergeMapsNil(t *testing.T) { - v := dyn.V(map[string]dyn.Value{ + l := dyn.Location{File: "file", Line: 1, Column: 2} + v := dyn.NewValue(map[string]dyn.Value{ "foo": dyn.V("bar"), - }) + }, []dyn.Location{l}) + + nilL := dyn.Location{File: "file", Line: 3, Column: 4} + nilV := dyn.NewValue(nil, []dyn.Location{nilL}) // Merge nil into v. { - out, err := Merge(v, dyn.NilValue) + out, err := Merge(v, nilV) assert.NoError(t, err) assert.Equal(t, map[string]any{ "foo": "bar", }, out.AsAny()) + + // Locations of both values should be preserved. + assert.Equal(t, []dyn.Location{l, nilL}, out.Locations()) + + // Location of the non-nil value should be returned by .Location(). + assert.Equal(t, l, out.Location()) } // Merge v into nil. { - out, err := Merge(dyn.NilValue, v) + out, err := Merge(nilV, v) assert.NoError(t, err) assert.Equal(t, map[string]any{ "foo": "bar", }, out.AsAny()) + + // Locations of both values should be preserved. + assert.Equal(t, []dyn.Location{l, nilL}, out.Locations()) + + // Location of the non-nil value should be returned by .Location(). + assert.Equal(t, l, out.Location()) } } @@ -81,15 +134,18 @@ func TestMergeMapsError(t *testing.T) { } func TestMergeSequences(t *testing.T) { - v1 := dyn.V([]dyn.Value{ - dyn.V("bar"), - dyn.V("baz"), - }) + l1 := dyn.Location{File: "file1", Line: 1, Column: 2} + v1 := dyn.NewValue([]dyn.Value{ + dyn.NewValue("bar", []dyn.Location{l1}), + dyn.NewValue("baz", []dyn.Location{l1}), + }, []dyn.Location{l1}) - v2 := dyn.V([]dyn.Value{ - dyn.V("qux"), - dyn.V("foo"), - }) + l2 := dyn.Location{File: "file2", Line: 3, Column: 4} + l3 := dyn.Location{File: "file3", Line: 5, Column: 6} + v2 := dyn.NewValue([]dyn.Value{ + dyn.NewValue("qux", []dyn.Location{l2}), + dyn.NewValue("foo", []dyn.Location{l3}), + }, []dyn.Location{l2, l3}) // Merge v2 into v1. { @@ -101,6 +157,18 @@ func TestMergeSequences(t *testing.T) { "qux", "foo", }, out.AsAny()) + + // Locations of both values should be preserved. + assert.Equal(t, []dyn.Location{l1, l2, l3}, out.Locations()) + + // Location of the merged value should be the location of v1. + assert.Equal(t, l1, out.Location()) + + // Location of the individual values should be preserved. + assert.Equal(t, l1, out.Index(0).Location()) // "bar" + assert.Equal(t, l1, out.Index(1).Location()) // "baz" + assert.Equal(t, l2, out.Index(2).Location()) // "qux" + assert.Equal(t, l3, out.Index(3).Location()) // "foo" } // Merge v1 into v2. @@ -113,6 +181,18 @@ func TestMergeSequences(t *testing.T) { "bar", "baz", }, out.AsAny()) + + // Locations of both values should be preserved. + assert.Equal(t, []dyn.Location{l2, l3, l1}, out.Locations()) + + // Location of the merged value should be the location of v2. + assert.Equal(t, l2, out.Location()) + + // Location of the individual values should be preserved. + assert.Equal(t, l2, out.Index(0).Location()) // "qux" + assert.Equal(t, l3, out.Index(1).Location()) // "foo" + assert.Equal(t, l1, out.Index(2).Location()) // "bar" + assert.Equal(t, l1, out.Index(3).Location()) // "baz" } } @@ -156,14 +236,22 @@ func TestMergeSequencesError(t *testing.T) { } func TestMergePrimitives(t *testing.T) { - v1 := dyn.V("bar") - v2 := dyn.V("baz") + l1 := dyn.Location{File: "file1", Line: 1, Column: 2} + l2 := dyn.Location{File: "file2", Line: 3, Column: 4} + v1 := dyn.NewValue("bar", []dyn.Location{l1}) + v2 := dyn.NewValue("baz", []dyn.Location{l2}) // Merge v2 into v1. { out, err := Merge(v1, v2) assert.NoError(t, err) assert.Equal(t, "baz", out.AsAny()) + + // Locations of both values should be preserved. + assert.Equal(t, []dyn.Location{l2, l1}, out.Locations()) + + // Location of the merged value should be the location of v2, the second value. + assert.Equal(t, l2, out.Location()) } // Merge v1 into v2. @@ -171,6 +259,12 @@ func TestMergePrimitives(t *testing.T) { out, err := Merge(v2, v1) assert.NoError(t, err) assert.Equal(t, "bar", out.AsAny()) + + // Locations of both values should be preserved. + assert.Equal(t, []dyn.Location{l1, l2}, out.Locations()) + + // Location of the merged value should be the location of v1, the second value. + assert.Equal(t, l1, out.Location()) } } diff --git a/libs/dyn/merge/override.go b/libs/dyn/merge/override.go index 823fb1933..7a8667cd6 100644 --- a/libs/dyn/merge/override.go +++ b/libs/dyn/merge/override.go @@ -51,7 +51,7 @@ func override(basePath dyn.Path, left dyn.Value, right dyn.Value, visitor Overri return dyn.InvalidValue, err } - return dyn.NewValue(merged, left.Location()), nil + return dyn.NewValue(merged, left.Locations()), nil case dyn.KindSequence: // some sequences are keyed, and we can detect which elements are added/removed/updated, @@ -62,7 +62,7 @@ func override(basePath dyn.Path, left dyn.Value, right dyn.Value, visitor Overri return dyn.InvalidValue, err } - return dyn.NewValue(merged, left.Location()), nil + return dyn.NewValue(merged, left.Locations()), nil case dyn.KindString: if left.MustString() == right.MustString() { diff --git a/libs/dyn/merge/override_test.go b/libs/dyn/merge/override_test.go index d9ca97486..9d41a526e 100644 --- a/libs/dyn/merge/override_test.go +++ b/libs/dyn/merge/override_test.go @@ -27,79 +27,79 @@ func TestOverride_Primitive(t *testing.T) { { name: "string (updated)", state: visitorState{updated: []string{"root"}}, - left: dyn.NewValue("a", leftLocation), - right: dyn.NewValue("b", rightLocation), - expected: dyn.NewValue("b", rightLocation), + left: dyn.NewValue("a", []dyn.Location{leftLocation}), + right: dyn.NewValue("b", []dyn.Location{rightLocation}), + expected: dyn.NewValue("b", []dyn.Location{rightLocation}), }, { name: "string (not updated)", state: visitorState{}, - left: dyn.NewValue("a", leftLocation), - right: dyn.NewValue("a", rightLocation), - expected: dyn.NewValue("a", leftLocation), + left: dyn.NewValue("a", []dyn.Location{leftLocation}), + right: dyn.NewValue("a", []dyn.Location{rightLocation}), + expected: dyn.NewValue("a", []dyn.Location{leftLocation}), }, { name: "bool (updated)", state: visitorState{updated: []string{"root"}}, - left: dyn.NewValue(true, leftLocation), - right: dyn.NewValue(false, rightLocation), - expected: dyn.NewValue(false, rightLocation), + left: dyn.NewValue(true, []dyn.Location{leftLocation}), + right: dyn.NewValue(false, []dyn.Location{rightLocation}), + expected: dyn.NewValue(false, []dyn.Location{rightLocation}), }, { name: "bool (not updated)", state: visitorState{}, - left: dyn.NewValue(true, leftLocation), - right: dyn.NewValue(true, rightLocation), - expected: dyn.NewValue(true, leftLocation), + left: dyn.NewValue(true, []dyn.Location{leftLocation}), + right: dyn.NewValue(true, []dyn.Location{rightLocation}), + expected: dyn.NewValue(true, []dyn.Location{leftLocation}), }, { name: "int (updated)", state: visitorState{updated: []string{"root"}}, - left: dyn.NewValue(1, leftLocation), - right: dyn.NewValue(2, rightLocation), - expected: dyn.NewValue(2, rightLocation), + left: dyn.NewValue(1, []dyn.Location{leftLocation}), + right: dyn.NewValue(2, []dyn.Location{rightLocation}), + expected: dyn.NewValue(2, []dyn.Location{rightLocation}), }, { name: "int (not updated)", state: visitorState{}, - left: dyn.NewValue(int32(1), leftLocation), - right: dyn.NewValue(int64(1), rightLocation), - expected: dyn.NewValue(int32(1), leftLocation), + left: dyn.NewValue(int32(1), []dyn.Location{leftLocation}), + right: dyn.NewValue(int64(1), []dyn.Location{rightLocation}), + expected: dyn.NewValue(int32(1), []dyn.Location{leftLocation}), }, { name: "float (updated)", state: visitorState{updated: []string{"root"}}, - left: dyn.NewValue(1.0, leftLocation), - right: dyn.NewValue(2.0, rightLocation), - expected: dyn.NewValue(2.0, rightLocation), + left: dyn.NewValue(1.0, []dyn.Location{leftLocation}), + right: dyn.NewValue(2.0, []dyn.Location{rightLocation}), + expected: dyn.NewValue(2.0, []dyn.Location{rightLocation}), }, { name: "float (not updated)", state: visitorState{}, - left: dyn.NewValue(float32(1.0), leftLocation), - right: dyn.NewValue(float64(1.0), rightLocation), - expected: dyn.NewValue(float32(1.0), leftLocation), + left: dyn.NewValue(float32(1.0), []dyn.Location{leftLocation}), + right: dyn.NewValue(float64(1.0), []dyn.Location{rightLocation}), + expected: dyn.NewValue(float32(1.0), []dyn.Location{leftLocation}), }, { name: "time (updated)", state: visitorState{updated: []string{"root"}}, - left: dyn.NewValue(time.UnixMilli(10000), leftLocation), - right: dyn.NewValue(time.UnixMilli(10001), rightLocation), - expected: dyn.NewValue(time.UnixMilli(10001), rightLocation), + left: dyn.NewValue(time.UnixMilli(10000), []dyn.Location{leftLocation}), + right: dyn.NewValue(time.UnixMilli(10001), []dyn.Location{rightLocation}), + expected: dyn.NewValue(time.UnixMilli(10001), []dyn.Location{rightLocation}), }, { name: "time (not updated)", state: visitorState{}, - left: dyn.NewValue(time.UnixMilli(10000), leftLocation), - right: dyn.NewValue(time.UnixMilli(10000), rightLocation), - expected: dyn.NewValue(time.UnixMilli(10000), leftLocation), + left: dyn.NewValue(time.UnixMilli(10000), []dyn.Location{leftLocation}), + right: dyn.NewValue(time.UnixMilli(10000), []dyn.Location{rightLocation}), + expected: dyn.NewValue(time.UnixMilli(10000), []dyn.Location{leftLocation}), }, { name: "different types (updated)", state: visitorState{updated: []string{"root"}}, - left: dyn.NewValue("a", leftLocation), - right: dyn.NewValue(42, rightLocation), - expected: dyn.NewValue(42, rightLocation), + left: dyn.NewValue("a", []dyn.Location{leftLocation}), + right: dyn.NewValue(42, []dyn.Location{rightLocation}), + expected: dyn.NewValue(42, []dyn.Location{rightLocation}), }, { name: "map - remove 'a', update 'b'", @@ -109,23 +109,22 @@ func TestOverride_Primitive(t *testing.T) { }, left: dyn.NewValue( map[string]dyn.Value{ - "a": dyn.NewValue(42, leftLocation), - "b": dyn.NewValue(10, leftLocation), + "a": dyn.NewValue(42, []dyn.Location{leftLocation}), + "b": dyn.NewValue(10, []dyn.Location{leftLocation}), }, - leftLocation, - ), + []dyn.Location{leftLocation}), + right: dyn.NewValue( map[string]dyn.Value{ - "b": dyn.NewValue(20, rightLocation), + "b": dyn.NewValue(20, []dyn.Location{rightLocation}), }, - rightLocation, - ), + []dyn.Location{rightLocation}), + expected: dyn.NewValue( map[string]dyn.Value{ - "b": dyn.NewValue(20, rightLocation), + "b": dyn.NewValue(20, []dyn.Location{rightLocation}), }, - leftLocation, - ), + []dyn.Location{leftLocation}), }, { name: "map - add 'a'", @@ -134,24 +133,26 @@ func TestOverride_Primitive(t *testing.T) { }, left: dyn.NewValue( map[string]dyn.Value{ - "b": dyn.NewValue(10, leftLocation), + "b": dyn.NewValue(10, []dyn.Location{leftLocation}), }, - leftLocation, + []dyn.Location{leftLocation}, ), + right: dyn.NewValue( map[string]dyn.Value{ - "a": dyn.NewValue(42, rightLocation), - "b": dyn.NewValue(10, rightLocation), + "a": dyn.NewValue(42, []dyn.Location{rightLocation}), + "b": dyn.NewValue(10, []dyn.Location{rightLocation}), }, - leftLocation, + []dyn.Location{leftLocation}, ), + expected: dyn.NewValue( map[string]dyn.Value{ - "a": dyn.NewValue(42, rightLocation), + "a": dyn.NewValue(42, []dyn.Location{rightLocation}), // location hasn't changed because value hasn't changed - "b": dyn.NewValue(10, leftLocation), + "b": dyn.NewValue(10, []dyn.Location{leftLocation}), }, - leftLocation, + []dyn.Location{leftLocation}, ), }, { @@ -161,23 +162,25 @@ func TestOverride_Primitive(t *testing.T) { }, left: dyn.NewValue( map[string]dyn.Value{ - "a": dyn.NewValue(42, leftLocation), - "b": dyn.NewValue(10, leftLocation), + "a": dyn.NewValue(42, []dyn.Location{leftLocation}), + "b": dyn.NewValue(10, []dyn.Location{leftLocation}), }, - leftLocation, + []dyn.Location{leftLocation}, ), + right: dyn.NewValue( map[string]dyn.Value{ - "b": dyn.NewValue(10, rightLocation), + "b": dyn.NewValue(10, []dyn.Location{rightLocation}), }, - leftLocation, + []dyn.Location{leftLocation}, ), + expected: dyn.NewValue( map[string]dyn.Value{ // location hasn't changed because value hasn't changed - "b": dyn.NewValue(10, leftLocation), + "b": dyn.NewValue(10, []dyn.Location{leftLocation}), }, - leftLocation, + []dyn.Location{leftLocation}, ), }, { @@ -189,36 +192,38 @@ func TestOverride_Primitive(t *testing.T) { map[string]dyn.Value{ "jobs": dyn.NewValue( map[string]dyn.Value{ - "job_0": dyn.NewValue(42, leftLocation), + "job_0": dyn.NewValue(42, []dyn.Location{leftLocation}), }, - leftLocation, + []dyn.Location{leftLocation}, ), }, - leftLocation, + []dyn.Location{leftLocation}, ), + right: dyn.NewValue( map[string]dyn.Value{ "jobs": dyn.NewValue( map[string]dyn.Value{ - "job_0": dyn.NewValue(42, rightLocation), - "job_1": dyn.NewValue(1337, rightLocation), + "job_0": dyn.NewValue(42, []dyn.Location{rightLocation}), + "job_1": dyn.NewValue(1337, []dyn.Location{rightLocation}), }, - rightLocation, + []dyn.Location{rightLocation}, ), }, - rightLocation, + []dyn.Location{rightLocation}, ), + expected: dyn.NewValue( map[string]dyn.Value{ "jobs": dyn.NewValue( map[string]dyn.Value{ - "job_0": dyn.NewValue(42, leftLocation), - "job_1": dyn.NewValue(1337, rightLocation), + "job_0": dyn.NewValue(42, []dyn.Location{leftLocation}), + "job_1": dyn.NewValue(1337, []dyn.Location{rightLocation}), }, - leftLocation, + []dyn.Location{leftLocation}, ), }, - leftLocation, + []dyn.Location{leftLocation}, ), }, { @@ -228,35 +233,35 @@ func TestOverride_Primitive(t *testing.T) { map[string]dyn.Value{ "jobs": dyn.NewValue( map[string]dyn.Value{ - "job_0": dyn.NewValue(42, leftLocation), - "job_1": dyn.NewValue(1337, rightLocation), + "job_0": dyn.NewValue(42, []dyn.Location{leftLocation}), + "job_1": dyn.NewValue(1337, []dyn.Location{rightLocation}), }, - leftLocation, + []dyn.Location{leftLocation}, ), }, - leftLocation, + []dyn.Location{leftLocation}, ), right: dyn.NewValue( map[string]dyn.Value{ "jobs": dyn.NewValue( map[string]dyn.Value{ - "job_0": dyn.NewValue(42, rightLocation), + "job_0": dyn.NewValue(42, []dyn.Location{rightLocation}), }, - rightLocation, + []dyn.Location{rightLocation}, ), }, - rightLocation, + []dyn.Location{rightLocation}, ), expected: dyn.NewValue( map[string]dyn.Value{ "jobs": dyn.NewValue( map[string]dyn.Value{ - "job_0": dyn.NewValue(42, leftLocation), + "job_0": dyn.NewValue(42, []dyn.Location{leftLocation}), }, - leftLocation, + []dyn.Location{leftLocation}, ), }, - leftLocation, + []dyn.Location{leftLocation}, ), }, { @@ -264,23 +269,23 @@ func TestOverride_Primitive(t *testing.T) { state: visitorState{added: []string{"root[1]"}}, left: dyn.NewValue( []dyn.Value{ - dyn.NewValue(42, leftLocation), + dyn.NewValue(42, []dyn.Location{leftLocation}), }, - leftLocation, + []dyn.Location{leftLocation}, ), right: dyn.NewValue( []dyn.Value{ - dyn.NewValue(42, rightLocation), - dyn.NewValue(10, rightLocation), + dyn.NewValue(42, []dyn.Location{rightLocation}), + dyn.NewValue(10, []dyn.Location{rightLocation}), }, - rightLocation, + []dyn.Location{rightLocation}, ), expected: dyn.NewValue( []dyn.Value{ - dyn.NewValue(42, leftLocation), - dyn.NewValue(10, rightLocation), + dyn.NewValue(42, []dyn.Location{leftLocation}), + dyn.NewValue(10, []dyn.Location{rightLocation}), }, - leftLocation, + []dyn.Location{leftLocation}, ), }, { @@ -288,67 +293,67 @@ func TestOverride_Primitive(t *testing.T) { state: visitorState{removed: []string{"root[1]"}}, left: dyn.NewValue( []dyn.Value{ - dyn.NewValue(42, leftLocation), - dyn.NewValue(10, leftLocation), + dyn.NewValue(42, []dyn.Location{leftLocation}), + dyn.NewValue(10, []dyn.Location{leftLocation}), }, - leftLocation, + []dyn.Location{leftLocation}, ), right: dyn.NewValue( []dyn.Value{ - dyn.NewValue(42, rightLocation), + dyn.NewValue(42, []dyn.Location{rightLocation}), }, - rightLocation, + []dyn.Location{rightLocation}, ), expected: dyn.NewValue( []dyn.Value{ - // location hasn't changed because value hasn't changed - dyn.NewValue(42, leftLocation), + dyn.NewValue(42, []dyn.Location{leftLocation}), }, - leftLocation, + []dyn.Location{leftLocation}, ), + // location hasn't changed because value hasn't changed }, { name: "sequence (not updated)", state: visitorState{}, left: dyn.NewValue( []dyn.Value{ - dyn.NewValue(42, leftLocation), + dyn.NewValue(42, []dyn.Location{leftLocation}), }, - leftLocation, + []dyn.Location{leftLocation}, ), right: dyn.NewValue( []dyn.Value{ - dyn.NewValue(42, rightLocation), + dyn.NewValue(42, []dyn.Location{rightLocation}), }, - rightLocation, + []dyn.Location{rightLocation}, ), expected: dyn.NewValue( []dyn.Value{ - dyn.NewValue(42, leftLocation), + dyn.NewValue(42, []dyn.Location{leftLocation}), }, - leftLocation, + []dyn.Location{leftLocation}, ), }, { name: "nil (not updated)", state: visitorState{}, - left: dyn.NilValue.WithLocation(leftLocation), - right: dyn.NilValue.WithLocation(rightLocation), - expected: dyn.NilValue.WithLocation(leftLocation), + left: dyn.NilValue.WithLocations([]dyn.Location{leftLocation}), + right: dyn.NilValue.WithLocations([]dyn.Location{rightLocation}), + expected: dyn.NilValue.WithLocations([]dyn.Location{leftLocation}), }, { name: "nil (updated)", state: visitorState{updated: []string{"root"}}, left: dyn.NilValue, - right: dyn.NewValue(42, rightLocation), - expected: dyn.NewValue(42, rightLocation), + right: dyn.NewValue(42, []dyn.Location{rightLocation}), + expected: dyn.NewValue(42, []dyn.Location{rightLocation}), }, { name: "change kind (updated)", state: visitorState{updated: []string{"root"}}, - left: dyn.NewValue(42.0, leftLocation), - right: dyn.NewValue(42, rightLocation), - expected: dyn.NewValue(42, rightLocation), + left: dyn.NewValue(42.0, []dyn.Location{leftLocation}), + right: dyn.NewValue(42, []dyn.Location{rightLocation}), + expected: dyn.NewValue(42, []dyn.Location{rightLocation}), }, } @@ -375,7 +380,7 @@ func TestOverride_Primitive(t *testing.T) { }) t.Run(tc.name+" - visitor overrides value", func(t *testing.T) { - expected := dyn.NewValue("return value", dyn.Location{}) + expected := dyn.V("return value") s, visitor := createVisitor(visitorOpts{returnValue: &expected}) out, err := override(dyn.EmptyPath, tc.left, tc.right, visitor) @@ -427,17 +432,17 @@ func TestOverride_PreserveMappingKeys(t *testing.T) { rightValueLocation := dyn.Location{File: "right.yml", Line: 3, Column: 1} left := dyn.NewMapping() - left.Set(dyn.NewValue("a", leftKeyLocation), dyn.NewValue(42, leftValueLocation)) + left.Set(dyn.NewValue("a", []dyn.Location{leftKeyLocation}), dyn.NewValue(42, []dyn.Location{leftValueLocation})) right := dyn.NewMapping() - right.Set(dyn.NewValue("a", rightKeyLocation), dyn.NewValue(7, rightValueLocation)) + right.Set(dyn.NewValue("a", []dyn.Location{rightKeyLocation}), dyn.NewValue(7, []dyn.Location{rightValueLocation})) state, visitor := createVisitor(visitorOpts{}) out, err := override( dyn.EmptyPath, - dyn.NewValue(left, leftLocation), - dyn.NewValue(right, rightLocation), + dyn.NewValue(left, []dyn.Location{leftLocation}), + dyn.NewValue(right, []dyn.Location{rightLocation}), visitor, ) diff --git a/libs/dyn/pattern.go b/libs/dyn/pattern.go index a265dad08..aecdc3ca6 100644 --- a/libs/dyn/pattern.go +++ b/libs/dyn/pattern.go @@ -72,7 +72,7 @@ func (c anyKeyComponent) visit(v Value, prefix Path, suffix Pattern, opts visitO m.Set(pk, nv) } - return NewValue(m, v.Location()), nil + return NewValue(m, v.Locations()), nil } type anyIndexComponent struct{} @@ -103,5 +103,5 @@ func (c anyIndexComponent) visit(v Value, prefix Path, suffix Pattern, opts visi s[i] = nv } - return NewValue(s, v.Location()), nil + return NewValue(s, v.Locations()), nil } diff --git a/libs/dyn/value.go b/libs/dyn/value.go index 3d62ea1f5..2aed2f6cd 100644 --- a/libs/dyn/value.go +++ b/libs/dyn/value.go @@ -2,13 +2,18 @@ package dyn import ( "fmt" + "slices" ) type Value struct { v any k Kind - l Location + + // List of locations this value is defined at. The first location in the slice + // is the location returned by the `.Location()` method and is typically used + // for reporting errors and warnings associated with the value. + l []Location // Whether or not this value is an anchor. // If this node doesn't map to a type, we don't need to warn about it. @@ -27,11 +32,11 @@ var NilValue = Value{ // V constructs a new Value with the given value. func V(v any) Value { - return NewValue(v, Location{}) + return NewValue(v, []Location{}) } // NewValue constructs a new Value with the given value and location. -func NewValue(v any, loc Location) Value { +func NewValue(v any, loc []Location) Value { switch vin := v.(type) { case map[string]Value: v = newMappingFromGoMap(vin) @@ -40,16 +45,30 @@ func NewValue(v any, loc Location) Value { return Value{ v: v, k: kindOf(v), - l: loc, + + // create a copy of the locations, so that mutations to the original slice + // don't affect new value. + l: slices.Clone(loc), } } -// WithLocation returns a new Value with its location set to the given value. -func (v Value) WithLocation(loc Location) Value { +// WithLocations returns a new Value with its location set to the given value. +func (v Value) WithLocations(loc []Location) Value { return Value{ v: v.v, k: v.k, - l: loc, + + // create a copy of the locations, so that mutations to the original slice + // don't affect new value. + l: slices.Clone(loc), + } +} + +func (v Value) AppendLocationsFromValue(w Value) Value { + return Value{ + v: v.v, + k: v.k, + l: append(v.l, w.l...), } } @@ -61,10 +80,18 @@ func (v Value) Value() any { return v.v } -func (v Value) Location() Location { +func (v Value) Locations() []Location { return v.l } +func (v Value) Location() Location { + if len(v.l) == 0 { + return Location{} + } + + return v.l[0] +} + func (v Value) IsValid() bool { return v.k != KindInvalid } @@ -153,7 +180,10 @@ func (v Value) IsAnchor() bool { // We need a custom implementation because maps and slices // cannot be compared with the regular == operator. func (v Value) eq(w Value) bool { - if v.k != w.k || v.l != w.l { + if v.k != w.k { + return false + } + if !slices.Equal(v.l, w.l) { return false } diff --git a/libs/dyn/value_test.go b/libs/dyn/value_test.go index bbdc2c96b..6a0a27b8d 100644 --- a/libs/dyn/value_test.go +++ b/libs/dyn/value_test.go @@ -25,16 +25,19 @@ func TestValueAsMap(t *testing.T) { _, ok := zeroValue.AsMap() assert.False(t, ok) - var intValue = dyn.NewValue(1, dyn.Location{}) + var intValue = dyn.V(1) _, ok = intValue.AsMap() assert.False(t, ok) var mapValue = dyn.NewValue( map[string]dyn.Value{ - "key": dyn.NewValue("value", dyn.Location{File: "file", Line: 1, Column: 2}), + "key": dyn.NewValue( + "value", + []dyn.Location{{File: "file", Line: 1, Column: 2}}), }, - dyn.Location{File: "file", Line: 1, Column: 2}, + []dyn.Location{{File: "file", Line: 1, Column: 2}}, ) + m, ok := mapValue.AsMap() assert.True(t, ok) assert.Equal(t, 1, m.Len()) @@ -43,6 +46,6 @@ func TestValueAsMap(t *testing.T) { func TestValueIsValid(t *testing.T) { var zeroValue dyn.Value assert.False(t, zeroValue.IsValid()) - var intValue = dyn.NewValue(1, dyn.Location{}) + var intValue = dyn.V(1) assert.True(t, intValue.IsValid()) } diff --git a/libs/dyn/value_underlying_test.go b/libs/dyn/value_underlying_test.go index 83cffb772..e35cde582 100644 --- a/libs/dyn/value_underlying_test.go +++ b/libs/dyn/value_underlying_test.go @@ -11,7 +11,7 @@ import ( func TestValueUnderlyingMap(t *testing.T) { v := dyn.V( map[string]dyn.Value{ - "key": dyn.NewValue("value", dyn.Location{File: "file", Line: 1, Column: 2}), + "key": dyn.NewValue("value", []dyn.Location{{File: "file", Line: 1, Column: 2}}), }, ) @@ -33,7 +33,7 @@ func TestValueUnderlyingMap(t *testing.T) { func TestValueUnderlyingSequence(t *testing.T) { v := dyn.V( []dyn.Value{ - dyn.NewValue("value", dyn.Location{File: "file", Line: 1, Column: 2}), + dyn.NewValue("value", []dyn.Location{{File: "file", Line: 1, Column: 2}}), }, ) diff --git a/libs/dyn/visit_map.go b/libs/dyn/visit_map.go index 56a9cf9f3..cd2cd4831 100644 --- a/libs/dyn/visit_map.go +++ b/libs/dyn/visit_map.go @@ -27,7 +27,7 @@ func Foreach(fn MapFunc) MapFunc { } m.Set(pk, nv) } - return NewValue(m, v.Location()), nil + return NewValue(m, v.Locations()), nil case KindSequence: s := slices.Clone(v.MustSequence()) for i, value := range s { @@ -37,7 +37,7 @@ func Foreach(fn MapFunc) MapFunc { return InvalidValue, err } } - return NewValue(s, v.Location()), nil + return NewValue(s, v.Locations()), nil default: return InvalidValue, fmt.Errorf("expected a map or sequence, found %s", v.Kind()) } diff --git a/libs/dyn/yamlloader/loader.go b/libs/dyn/yamlloader/loader.go index e6a16f79e..fbb52b504 100644 --- a/libs/dyn/yamlloader/loader.go +++ b/libs/dyn/yamlloader/loader.go @@ -86,7 +86,7 @@ func (d *loader) loadSequence(node *yaml.Node, loc dyn.Location) (dyn.Value, err acc[i] = v } - return dyn.NewValue(acc, loc), nil + return dyn.NewValue(acc, []dyn.Location{loc}), nil } func (d *loader) loadMapping(node *yaml.Node, loc dyn.Location) (dyn.Value, error) { @@ -130,7 +130,7 @@ func (d *loader) loadMapping(node *yaml.Node, loc dyn.Location) (dyn.Value, erro } if merge == nil { - return dyn.NewValue(acc, loc), nil + return dyn.NewValue(acc, []dyn.Location{loc}), nil } // Build location for the merge node. @@ -171,20 +171,20 @@ func (d *loader) loadMapping(node *yaml.Node, loc dyn.Location) (dyn.Value, erro out.Merge(m) } - return dyn.NewValue(out, loc), nil + return dyn.NewValue(out, []dyn.Location{loc}), nil } func (d *loader) loadScalar(node *yaml.Node, loc dyn.Location) (dyn.Value, error) { st := node.ShortTag() switch st { case "!!str": - return dyn.NewValue(node.Value, loc), nil + return dyn.NewValue(node.Value, []dyn.Location{loc}), nil case "!!bool": switch strings.ToLower(node.Value) { case "true": - return dyn.NewValue(true, loc), nil + return dyn.NewValue(true, []dyn.Location{loc}), nil case "false": - return dyn.NewValue(false, loc), nil + return dyn.NewValue(false, []dyn.Location{loc}), nil default: return dyn.InvalidValue, errorf(loc, "invalid bool value: %v", node.Value) } @@ -195,17 +195,17 @@ func (d *loader) loadScalar(node *yaml.Node, loc dyn.Location) (dyn.Value, error } // Use regular int type instead of int64 if possible. if i64 >= math.MinInt32 && i64 <= math.MaxInt32 { - return dyn.NewValue(int(i64), loc), nil + return dyn.NewValue(int(i64), []dyn.Location{loc}), nil } - return dyn.NewValue(i64, loc), nil + return dyn.NewValue(i64, []dyn.Location{loc}), nil case "!!float": f64, err := strconv.ParseFloat(node.Value, 64) if err != nil { return dyn.InvalidValue, errorf(loc, "invalid float value: %v", node.Value) } - return dyn.NewValue(f64, loc), nil + return dyn.NewValue(f64, []dyn.Location{loc}), nil case "!!null": - return dyn.NewValue(nil, loc), nil + return dyn.NewValue(nil, []dyn.Location{loc}), nil case "!!timestamp": // Try a couple of layouts for _, layout := range []string{ @@ -216,7 +216,7 @@ func (d *loader) loadScalar(node *yaml.Node, loc dyn.Location) (dyn.Value, error } { t, terr := time.Parse(layout, node.Value) if terr == nil { - return dyn.NewValue(t, loc), nil + return dyn.NewValue(t, []dyn.Location{loc}), nil } } return dyn.InvalidValue, errorf(loc, "invalid timestamp value: %v", node.Value) diff --git a/libs/dyn/yamlsaver/saver_test.go b/libs/dyn/yamlsaver/saver_test.go index bdf1891cd..387090104 100644 --- a/libs/dyn/yamlsaver/saver_test.go +++ b/libs/dyn/yamlsaver/saver_test.go @@ -19,7 +19,7 @@ func TestMarshalNilValue(t *testing.T) { func TestMarshalIntValue(t *testing.T) { s := NewSaver() - var intValue = dyn.NewValue(1, dyn.Location{}) + var intValue = dyn.V(1) v, err := s.toYamlNode(intValue) assert.NoError(t, err) assert.Equal(t, "1", v.Value) @@ -28,7 +28,7 @@ func TestMarshalIntValue(t *testing.T) { func TestMarshalFloatValue(t *testing.T) { s := NewSaver() - var floatValue = dyn.NewValue(1.0, dyn.Location{}) + var floatValue = dyn.V(1.0) v, err := s.toYamlNode(floatValue) assert.NoError(t, err) assert.Equal(t, "1", v.Value) @@ -37,7 +37,7 @@ func TestMarshalFloatValue(t *testing.T) { func TestMarshalBoolValue(t *testing.T) { s := NewSaver() - var boolValue = dyn.NewValue(true, dyn.Location{}) + var boolValue = dyn.V(true) v, err := s.toYamlNode(boolValue) assert.NoError(t, err) assert.Equal(t, "true", v.Value) @@ -46,7 +46,7 @@ func TestMarshalBoolValue(t *testing.T) { func TestMarshalTimeValue(t *testing.T) { s := NewSaver() - var timeValue = dyn.NewValue(time.Unix(0, 0), dyn.Location{}) + var timeValue = dyn.V(time.Unix(0, 0)) v, err := s.toYamlNode(timeValue) assert.NoError(t, err) assert.Equal(t, "1970-01-01 00:00:00 +0000 UTC", v.Value) @@ -57,10 +57,10 @@ func TestMarshalSequenceValue(t *testing.T) { s := NewSaver() var sequenceValue = dyn.NewValue( []dyn.Value{ - dyn.NewValue("value1", dyn.Location{File: "file", Line: 1, Column: 2}), - dyn.NewValue("value2", dyn.Location{File: "file", Line: 2, Column: 2}), + dyn.NewValue("value1", []dyn.Location{{File: "file", Line: 1, Column: 2}}), + dyn.NewValue("value2", []dyn.Location{{File: "file", Line: 2, Column: 2}}), }, - dyn.Location{File: "file", Line: 1, Column: 2}, + []dyn.Location{{File: "file", Line: 1, Column: 2}}, ) v, err := s.toYamlNode(sequenceValue) assert.NoError(t, err) @@ -71,7 +71,7 @@ func TestMarshalSequenceValue(t *testing.T) { func TestMarshalStringValue(t *testing.T) { s := NewSaver() - var stringValue = dyn.NewValue("value", dyn.Location{}) + var stringValue = dyn.V("value") v, err := s.toYamlNode(stringValue) assert.NoError(t, err) assert.Equal(t, "value", v.Value) @@ -82,12 +82,13 @@ func TestMarshalMapValue(t *testing.T) { s := NewSaver() var mapValue = dyn.NewValue( map[string]dyn.Value{ - "key3": dyn.NewValue("value3", dyn.Location{File: "file", Line: 3, Column: 2}), - "key2": dyn.NewValue("value2", dyn.Location{File: "file", Line: 2, Column: 2}), - "key1": dyn.NewValue("value1", dyn.Location{File: "file", Line: 1, Column: 2}), + "key3": dyn.NewValue("value3", []dyn.Location{{File: "file", Line: 3, Column: 2}}), + "key2": dyn.NewValue("value2", []dyn.Location{{File: "file", Line: 2, Column: 2}}), + "key1": dyn.NewValue("value1", []dyn.Location{{File: "file", Line: 1, Column: 2}}), }, - dyn.Location{File: "file", Line: 1, Column: 2}, + []dyn.Location{{File: "file", Line: 1, Column: 2}}, ) + v, err := s.toYamlNode(mapValue) assert.NoError(t, err) assert.Equal(t, yaml.MappingNode, v.Kind) @@ -107,12 +108,12 @@ func TestMarshalNestedValues(t *testing.T) { map[string]dyn.Value{ "key1": dyn.NewValue( map[string]dyn.Value{ - "key2": dyn.NewValue("value", dyn.Location{File: "file", Line: 1, Column: 2}), + "key2": dyn.NewValue("value", []dyn.Location{{File: "file", Line: 1, Column: 2}}), }, - dyn.Location{File: "file", Line: 1, Column: 2}, + []dyn.Location{{File: "file", Line: 1, Column: 2}}, ), }, - dyn.Location{File: "file", Line: 1, Column: 2}, + []dyn.Location{{File: "file", Line: 1, Column: 2}}, ) v, err := s.toYamlNode(mapValue) assert.NoError(t, err) @@ -125,14 +126,14 @@ func TestMarshalNestedValues(t *testing.T) { func TestMarshalHexadecimalValueIsQuoted(t *testing.T) { s := NewSaver() - var hexValue = dyn.NewValue(0x123, dyn.Location{}) + var hexValue = dyn.V(0x123) v, err := s.toYamlNode(hexValue) assert.NoError(t, err) assert.Equal(t, "291", v.Value) assert.Equal(t, yaml.Style(0), v.Style) assert.Equal(t, yaml.ScalarNode, v.Kind) - var stringValue = dyn.NewValue("0x123", dyn.Location{}) + var stringValue = dyn.V("0x123") v, err = s.toYamlNode(stringValue) assert.NoError(t, err) assert.Equal(t, "0x123", v.Value) @@ -142,14 +143,14 @@ func TestMarshalHexadecimalValueIsQuoted(t *testing.T) { func TestMarshalBinaryValueIsQuoted(t *testing.T) { s := NewSaver() - var binaryValue = dyn.NewValue(0b101, dyn.Location{}) + var binaryValue = dyn.V(0b101) v, err := s.toYamlNode(binaryValue) assert.NoError(t, err) assert.Equal(t, "5", v.Value) assert.Equal(t, yaml.Style(0), v.Style) assert.Equal(t, yaml.ScalarNode, v.Kind) - var stringValue = dyn.NewValue("0b101", dyn.Location{}) + var stringValue = dyn.V("0b101") v, err = s.toYamlNode(stringValue) assert.NoError(t, err) assert.Equal(t, "0b101", v.Value) @@ -159,14 +160,14 @@ func TestMarshalBinaryValueIsQuoted(t *testing.T) { func TestMarshalOctalValueIsQuoted(t *testing.T) { s := NewSaver() - var octalValue = dyn.NewValue(0123, dyn.Location{}) + var octalValue = dyn.V(0123) v, err := s.toYamlNode(octalValue) assert.NoError(t, err) assert.Equal(t, "83", v.Value) assert.Equal(t, yaml.Style(0), v.Style) assert.Equal(t, yaml.ScalarNode, v.Kind) - var stringValue = dyn.NewValue("0123", dyn.Location{}) + var stringValue = dyn.V("0123") v, err = s.toYamlNode(stringValue) assert.NoError(t, err) assert.Equal(t, "0123", v.Value) @@ -176,14 +177,14 @@ func TestMarshalOctalValueIsQuoted(t *testing.T) { func TestMarshalFloatValueIsQuoted(t *testing.T) { s := NewSaver() - var floatValue = dyn.NewValue(1.0, dyn.Location{}) + var floatValue = dyn.V(1.0) v, err := s.toYamlNode(floatValue) assert.NoError(t, err) assert.Equal(t, "1", v.Value) assert.Equal(t, yaml.Style(0), v.Style) assert.Equal(t, yaml.ScalarNode, v.Kind) - var stringValue = dyn.NewValue("1.0", dyn.Location{}) + var stringValue = dyn.V("1.0") v, err = s.toYamlNode(stringValue) assert.NoError(t, err) assert.Equal(t, "1.0", v.Value) @@ -193,14 +194,14 @@ func TestMarshalFloatValueIsQuoted(t *testing.T) { func TestMarshalBoolValueIsQuoted(t *testing.T) { s := NewSaver() - var boolValue = dyn.NewValue(true, dyn.Location{}) + var boolValue = dyn.V(true) v, err := s.toYamlNode(boolValue) assert.NoError(t, err) assert.Equal(t, "true", v.Value) assert.Equal(t, yaml.Style(0), v.Style) assert.Equal(t, yaml.ScalarNode, v.Kind) - var stringValue = dyn.NewValue("true", dyn.Location{}) + var stringValue = dyn.V("true") v, err = s.toYamlNode(stringValue) assert.NoError(t, err) assert.Equal(t, "true", v.Value) @@ -215,18 +216,18 @@ func TestCustomStylingWithNestedMap(t *testing.T) { var styledMap = dyn.NewValue( map[string]dyn.Value{ - "key1": dyn.NewValue("value1", dyn.Location{File: "file", Line: 1, Column: 2}), - "key2": dyn.NewValue("value2", dyn.Location{File: "file", Line: 2, Column: 2}), + "key1": dyn.NewValue("value1", []dyn.Location{{File: "file", Line: 1, Column: 2}}), + "key2": dyn.NewValue("value2", []dyn.Location{{File: "file", Line: 2, Column: 2}}), }, - dyn.Location{File: "file", Line: -2, Column: 2}, + []dyn.Location{{File: "file", Line: -2, Column: 2}}, ) var unstyledMap = dyn.NewValue( map[string]dyn.Value{ - "key3": dyn.NewValue("value3", dyn.Location{File: "file", Line: 1, Column: 2}), - "key4": dyn.NewValue("value4", dyn.Location{File: "file", Line: 2, Column: 2}), + "key3": dyn.NewValue("value3", []dyn.Location{{File: "file", Line: 1, Column: 2}}), + "key4": dyn.NewValue("value4", []dyn.Location{{File: "file", Line: 2, Column: 2}}), }, - dyn.Location{File: "file", Line: -1, Column: 2}, + []dyn.Location{{File: "file", Line: -1, Column: 2}}, ) var val = dyn.NewValue( @@ -234,7 +235,7 @@ func TestCustomStylingWithNestedMap(t *testing.T) { "styled": styledMap, "unstyled": unstyledMap, }, - dyn.Location{File: "file", Line: 1, Column: 2}, + []dyn.Location{{File: "file", Line: 1, Column: 2}}, ) mv, err := s.toYamlNode(val) diff --git a/libs/dyn/yamlsaver/utils.go b/libs/dyn/yamlsaver/utils.go index fa5ab08fb..a162bf31f 100644 --- a/libs/dyn/yamlsaver/utils.go +++ b/libs/dyn/yamlsaver/utils.go @@ -44,7 +44,7 @@ func skipAndOrder(mv dyn.Value, order *Order, skipFields []string, dst map[strin continue } - dst[k] = dyn.NewValue(v.Value(), dyn.Location{Line: order.Get(k)}) + dst[k] = dyn.NewValue(v.Value(), []dyn.Location{{Line: order.Get(k)}}) } return dyn.V(dst), nil diff --git a/libs/dyn/yamlsaver/utils_test.go b/libs/dyn/yamlsaver/utils_test.go index 04b4c404f..1afab601a 100644 --- a/libs/dyn/yamlsaver/utils_test.go +++ b/libs/dyn/yamlsaver/utils_test.go @@ -33,16 +33,25 @@ func TestConvertToMapValueWithOrder(t *testing.T) { assert.NoError(t, err) assert.Equal(t, dyn.V(map[string]dyn.Value{ - "list": dyn.NewValue([]dyn.Value{ - dyn.V("a"), - dyn.V("b"), - dyn.V("c"), - }, dyn.Location{Line: -3}), - "name": dyn.NewValue("test", dyn.Location{Line: -2}), - "map": dyn.NewValue(map[string]dyn.Value{ - "key1": dyn.V("value1"), - "key2": dyn.V("value2"), - }, dyn.Location{Line: -1}), - "long_name_field": dyn.NewValue("long name goes here", dyn.Location{Line: 1}), + "list": dyn.NewValue( + []dyn.Value{ + dyn.V("a"), + dyn.V("b"), + dyn.V("c"), + }, + []dyn.Location{{Line: -3}}, + ), + "name": dyn.NewValue( + "test", + []dyn.Location{{Line: -2}}, + ), + "map": dyn.NewValue( + map[string]dyn.Value{ + "key1": dyn.V("value1"), + "key2": dyn.V("value2"), + }, + []dyn.Location{{Line: -1}}, + ), + "long_name_field": dyn.NewValue("long name goes here", []dyn.Location{{Line: 1}}), }), result) }