diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 1fd62eb..f65fe73 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -120,7 +120,7 @@ Roadmap candidate 1. Owner: `internal/engine/metadata.go`. Validate: `go test ./internal/engine -run 'Metadata|Presence|Explicit|Inherited'`, relevant root facade tests, and the everyday check. No allocation assertion. -## Stage 3: Detect inheritance without constructing paths +## Stage 3: Detect inheritance without constructing paths — Complete Roadmap candidate 9. Owner: `internal/engine/inheritance_load.go`. diff --git a/internal/engine/inheritance_load.go b/internal/engine/inheritance_load.go index cebe16a..b7dd9af 100644 --- a/internal/engine/inheritance_load.go +++ b/internal/engine/inheritance_load.go @@ -3,14 +3,28 @@ package engine import "gitea.maximumdirect.net/eric/ConfigKit/internal/document" func containsInheritanceDirective(root *document.Node) bool { - found := false - walkDocument(root, Path{}, func(path Path, _ *document.Node) { - if len(path.segments) != 0 && path.segments[len(path.segments)-1].kind == pathSegmentKey && - path.segments[len(path.segments)-1].key == inheritanceKey { - found = true + var visit func(*document.Node) bool + visit = func(node *document.Node) bool { + if node == nil { + return false } - }) - return found + switch node.Kind { + case document.Mapping: + for _, entry := range node.Mapping { + if entry.Key == inheritanceKey || visit(entry.Value) { + return true + } + } + case document.Sequence: + for _, child := range node.Sequence { + if visit(child) { + return true + } + } + } + return false + } + return visit(root) } func inheritanceVersionDiagnostic(rule *VersionRule, expanded *inheritanceScope) *Diagnostic { diff --git a/internal/engine/inheritance_test.go b/internal/engine/inheritance_test.go index ac1faf4..50e6ab8 100644 --- a/internal/engine/inheritance_test.go +++ b/internal/engine/inheritance_test.go @@ -121,6 +121,24 @@ func TestSiblingInheritanceRejectsInvalidRelationships(t *testing.T) { } } +func TestLoadDetectsInvalidInheritanceInsideSequence(t *testing.T) { + input := []byte(`items: + - child: {extends: true} +`) + result, err := testLoad[map[string]any](Bytes("nested-inheritance.yml", input), Options[map[string]any]{}) + if result != nil || err == nil { + t.Fatalf("result, err = %#v, %v; want invalid nested inheritance", result, err) + } + loadError, ok := err.(*LoadError) + if !ok { + t.Fatalf("error type = %T, want *LoadError", err) + } + diagnostics := loadError.Diagnostics() + if len(diagnostics) != 1 || diagnostics[0].Code != CodeInheritanceInvalid || diagnostics[0].Path.String() != "/items/0/child/extends" { + t.Fatalf("diagnostics = %#v", diagnostics) + } +} + func TestSiblingInheritanceDetectsDeepCycleWithoutRecursiveDependencyWalk(t *testing.T) { const count = 2048 entries := make([]document.Entry, count)