Distinguish finite slice views from cycles

This commit is contained in:
2026-10-25 21:22:01 +00:00
parent deb9aa5c69
commit ae3408a096
3 changed files with 86 additions and 0 deletions

View File

@@ -139,6 +139,9 @@ Focused check: `go test ./internal/engine -run 'Dynamic|Projection|Clone|Resolut
Finding: **A03**. Finding: **A03**.
Owners: `internal/engine/value_operation.go`, `value.go`, `clone.go`, and tests. Owners: `internal/engine/value_operation.go`, `value.go`, `clone.go`, and tests.
Status: **complete** (A03). Prior stage commits: `2736ddf`, `deb9aa5`.
Validation passed: `GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go test ./internal/engine -run 'Mapping|Value|Projection|Clone|Override|Cycle|FiniteSlice|SliceCycles' -count=1` and `GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go test ./internal/engine -count=1`.
Add visible slice length to `valueVisit` identity in `visitFor`. Keep type, kind, Add visible slice length to `valueVisit` identity in `visitFor`. Keep type, kind,
and starting pointer, existing empty/nil handling, and active-path removal on and starting pointer, existing empty/nil handling, and active-path removal on
unwinding. Capacity is not part of the visible traversed graph. Maps and pointers unwinding. Capacity is not part of the visible traversed graph. Maps and pointers

View File

@@ -0,0 +1,79 @@
package engine
import (
"reflect"
"testing"
)
type sliceViewConfig struct {
Value []any `yaml:"value"`
}
func finiteSliceView() []any {
shared := []any{"leaf", nil}
shared[1] = shared[:1]
return shared
}
func TestFiniteSliceViewsAreAcceptedAcrossValueBoundaries(t *testing.T) {
want := []any{"leaf", []any{"leaf"}}
mappingSource := Mapping("mapping.yml", map[string]any{"value": finiteSliceView()})
mapping, err := testLoad[sliceViewConfig](mappingSource)
if err != nil || !reflect.DeepEqual(mapping.Config.Value, want) {
t.Fatalf("mapping value = %#v, error = %v", mapping, err)
}
defaults, err := testLoad[sliceViewConfig](NoSource(), Options[sliceViewConfig]{Definition: Definition[sliceViewConfig]{
Defaults: func() sliceViewConfig { return sliceViewConfig{Value: finiteSliceView()} },
}})
if err != nil || !reflect.DeepEqual(defaults.Config.Value, want) {
t.Fatalf("typed default value = %#v, error = %v", defaults, err)
}
override, err := testLoad[sliceViewConfig](NoSource(), Options[sliceViewConfig]{Overrides: []Override{{
Path: Keys("value"), Value: finiteSliceView(), Flag: "--value",
}}})
if err != nil || !reflect.DeepEqual(override.Config.Value, want) {
t.Fatalf("CLI override value = %#v, error = %v", override, err)
}
original := sliceViewConfig{Value: finiteSliceView()}
schema := mustSchemaFor[sliceViewConfig](t)
projected, issue := projectConfiguration(schema, reflect.ValueOf(original), defaultProjectionLimits())
if issue != nil {
t.Fatalf("finite view projection issue = %#v", issue)
}
cloned, issue := cloneConfiguration(schema, reflect.ValueOf(original), defaultProjectionLimits())
if issue != nil {
t.Fatalf("finite view clone issue = %#v", issue)
}
if !reflect.DeepEqual(cloned.Interface(), original) || !reflect.DeepEqual(projected, mappingSource.mapping) {
t.Fatalf("equivalent finite values differ: clone=%#v projected=%#v mapping=%#v", cloned.Interface(), projected, mappingSource.mapping)
}
cloned.FieldByName("Value").Index(1).Interface().([]any)[0] = "changed"
if original.Value[0] != "leaf" || original.Value[1].([]any)[0] != "leaf" {
t.Fatal("mutating a cloned descendant changed the original value")
}
}
func TestSliceCyclesRemainRejected(t *testing.T) {
schema := mustSchemaFor[sliceViewConfig](t)
self := make([]any, 1)
self[0] = self
first := make([]any, 1)
second := make([]any, 1)
first[0], second[0] = second, first
for name, value := range map[string][]any{"self": self, "mutual": first} {
t.Run(name, func(t *testing.T) {
configuration := sliceViewConfig{Value: value}
if _, issue := projectConfiguration(schema, reflect.ValueOf(configuration), defaultProjectionLimits()); issue == nil || issue.code != CodeSchemaCycle {
t.Fatalf("projection cycle issue = %#v", issue)
}
if _, issue := cloneConfiguration(schema, reflect.ValueOf(configuration), defaultProjectionLimits()); issue == nil || issue.code != CodeSchemaCycle {
t.Fatalf("clone cycle issue = %#v", issue)
}
})
}
}

View File

@@ -21,6 +21,7 @@ type valueVisit struct {
typeOf reflect.Type typeOf reflect.Type
kind reflect.Kind kind reflect.Kind
ptr uintptr ptr uintptr
length int
} }
// valueOperation owns one traversal's cycle and budget state and reuses the // valueOperation owns one traversal's cycle and budget state and reuses the
@@ -107,6 +108,9 @@ func visitFor(value reflect.Value) (valueVisit, bool) {
return valueVisit{}, false return valueVisit{}, false
} }
visit.ptr = value.Pointer() visit.ptr = value.Pointer()
if value.Kind() == reflect.Slice {
visit.length = value.Len()
}
case reflect.Map: case reflect.Map:
if value.Len() == 0 { if value.Len() == 0 {
return valueVisit{}, false return valueVisit{}, false