Share semantic document traversal helpers

This commit is contained in:
2026-09-07 01:07:02 +00:00
parent fc2b6c81a1
commit 633c5cd519
7 changed files with 83 additions and 85 deletions

View File

@@ -8,7 +8,8 @@ module and package documentation boundary. Its implemented public primitives are
- canonical, control-safe diagnostic rendering and structured `LoadError` access; - canonical, control-safe diagnostic rendering and structured `LoadError` access;
- opaque file, byte, reader, and no-input source construction in `source.go`, - opaque file, byte, reader, and no-input source construction in `source.go`,
with shared regular-file checks and bounded read mechanics in `source_file.go`; with shared regular-file checks and bounded read mechanics in `source_file.go`;
- a parser-independent ordered tree in `internal/document`; - a parser-independent ordered tree in `internal/document`, with root-private
semantic lookup, indexing, and value traversal in `document_walk.go`;
- the sole upstream YAML integration in `internal/yaml`, which translates validated - the sole upstream YAML integration in `internal/yaml`, which translates validated
input into that owned tree and exposes no parser types outside its boundary; input into that owned tree and exposes no parser types outside its boundary;
`parse.go` owns shared translation into detached, ordered public diagnostics; `parse.go` owns shared translation into detached, ordered public diagnostics;

View File

@@ -163,7 +163,14 @@ reliably induce an important filesystem failure. Run race tests for this unit.
## Stage 4 — Establish shared document traversal ownership ## Stage 4 — Establish shared document traversal ownership
Status: pending. Status: complete.
Completion: `document_walk.go` owns nil-safe preorder value walking, borrowed
indexing, and semantic lookup. Metadata shapes and projected sensitivity use the
walker with defensive path copies. Specialized origins and budget traversals
remain explicit. Full offline tests (including historical path shapes, metadata
detachment, and dynamic secrets), vet, build, and whitespace checks passed with
Go 1.26.7 on Linux/amd64.
Evidence: `indexDocument`, `walkDocumentPaths`, and `projectedSensitivity` repeat Evidence: `indexDocument`, `walkDocumentPaths`, and `projectedSensitivity` repeat
semantic tree traversal in [load_metadata.go](../../load_metadata.go), semantic tree traversal in [load_metadata.go](../../load_metadata.go),

66
document_walk.go Normal file
View File

@@ -0,0 +1,66 @@
package configkit
import "gitea.maximumdirect.net/eric/ConfigKit/internal/document"
// walkDocument visits value nodes in preorder, preserving stored mapping order
// and sequence index order. Mapping keys are not separate nodes in this walk.
// The callback borrows paths and nodes; clone any mutable state it retains.
// Specialized origin, pruning, and budget traversals retain their own semantics.
func walkDocument(node *document.Node, path Path, visit func(Path, *document.Node)) {
if node == nil {
return
}
visit(path, node)
switch node.Kind {
case document.Mapping:
for _, entry := range node.Mapping {
walkDocument(entry.Value, path.Key(entry.Key), visit)
}
case document.Sequence:
for index, child := range node.Sequence {
walkDocument(child, path.Index(index), visit)
}
}
}
// indexDocument borrows nodes from its caller-owned tree.
func indexDocument(node *document.Node, path Path, index map[string]*document.Node) {
walkDocument(node, path, func(path Path, node *document.Node) {
index[path.String()] = node
})
}
func documentAtPath(root *document.Node, path Path) (*document.Node, bool) {
if root == nil || !path.valid() {
return nil, false
}
current := root
for _, segment := range path.segments {
switch current.Kind {
case document.Mapping:
if segment.kind == pathSegmentIndex {
return nil, false
}
found := false
for _, entry := range current.Mapping {
if entry.Key == segment.key {
current = entry.Value
found = true
break
}
}
if !found {
return nil, false
}
case document.Sequence:
index, ok := schemaIndex(segment)
if !ok || index >= len(current.Sequence) {
return nil, false
}
current = current.Sequence[index]
default:
return nil, false
}
}
return current, true
}

35
load.go
View File

@@ -400,41 +400,6 @@ func checkVersion(rule *VersionRule, root *document.Node) []Diagnostic {
return canonicalDiagnostics(state.diagnostics) return canonicalDiagnostics(state.diagnostics)
} }
func documentAtPath(root *document.Node, path Path) (*document.Node, bool) {
if root == nil || !path.valid() {
return nil, false
}
current := root
for _, segment := range path.segments {
switch current.Kind {
case document.Mapping:
if segment.kind == pathSegmentIndex {
return nil, false
}
found := false
for _, entry := range current.Mapping {
if entry.Key == segment.key {
current = entry.Value
found = true
break
}
}
if !found {
return nil, false
}
case document.Sequence:
index, ok := schemaIndex(segment)
if !ok || index >= len(current.Sequence) {
return nil, false
}
current = current.Sequence[index]
default:
return nil, false
}
}
return current, true
}
func documentLocationPointer(location document.Location) *Location { func documentLocationPointer(location document.Location) *Location {
if !locationKnown(location) { if !locationKnown(location) {
return nil return nil

View File

@@ -159,23 +159,6 @@ func collectFieldInfo(node *document.Node, path Path, decoded decodedConfigurati
} }
} }
func indexDocument(node *document.Node, path Path, index map[string]*document.Node) {
if node == nil {
return
}
index[path.String()] = node
switch node.Kind {
case document.Mapping:
for _, entry := range node.Mapping {
indexDocument(entry.Value, path.Key(entry.Key), index)
}
case document.Sequence:
for position, child := range node.Sequence {
indexDocument(child, path.Index(position), index)
}
}
}
func cloneMetadata(metadata Metadata) Metadata { func cloneMetadata(metadata Metadata) Metadata {
if metadata.fields == nil && metadata.history == nil { if metadata.fields == nil && metadata.history == nil {
return Metadata{} return Metadata{}

View File

@@ -85,8 +85,8 @@ func (m Metadata) hasHistoricalPath(path Path) bool {
func finalizeMetadataPaths(m *Metadata, final *document.Node, applications []historyApplication, earlier ...*document.Node) { func finalizeMetadataPaths(m *Metadata, final *document.Node, applications []historyApplication, earlier ...*document.Node) {
m.effectivePaths = make(map[string]Path) m.effectivePaths = make(map[string]Path)
m.historicalPaths = make(map[string][]Path) m.historicalPaths = make(map[string][]Path)
walkDocumentPaths(final, Path{}, func(path Path) { m.effectivePaths[path.String()] = path.clone() }) walkDocument(final, Path{}, func(path Path, _ *document.Node) { m.effectivePaths[path.String()] = path.clone() })
add := func(path Path) { add := func(path Path, _ *document.Node) {
pointer := path.String() pointer := path.String()
for _, prior := range m.historicalPaths[pointer] { for _, prior := range m.historicalPaths[pointer] {
if matchesPathShape(path, prior) { if matchesPathShape(path, prior) {
@@ -95,9 +95,9 @@ func finalizeMetadataPaths(m *Metadata, final *document.Node, applications []his
} }
m.historicalPaths[pointer] = append(m.historicalPaths[pointer], path.clone()) m.historicalPaths[pointer] = append(m.historicalPaths[pointer], path.clone())
} }
walkDocumentPaths(final, Path{}, add) walkDocument(final, Path{}, add)
for _, tree := range earlier { for _, tree := range earlier {
walkDocumentPaths(tree, Path{}, add) walkDocument(tree, Path{}, add)
} }
for _, application := range applications { for _, application := range applications {
// Overrides have mapping-only prefixes; parsed tokens are keys in this layer. // Overrides have mapping-only prefixes; parsed tokens are keys in this layer.
@@ -105,19 +105,6 @@ func finalizeMetadataPaths(m *Metadata, final *document.Node, applications []his
for _, segment := range application.path.segments { for _, segment := range application.path.segments {
prefix = prefix.Key(segment.key) prefix = prefix.Key(segment.key)
} }
walkDocumentPaths(application.value, prefix, add) walkDocument(application.value, prefix, add)
}
}
func walkDocumentPaths(node *document.Node, path Path, visit func(Path)) {
if node == nil {
return
}
visit(path)
for _, entry := range node.Mapping {
walkDocumentPaths(entry.Value, path.Key(entry.Key), visit)
}
for i, child := range node.Sequence {
walkDocumentPaths(child, path.Index(i), visit)
} }
} }

View File

@@ -144,21 +144,10 @@ func schemaSecretRoot(schema *schemaDescriptor, path Path) (Path, bool) {
// is equal. Validators must preserve this inspection metadata as well as values. // is equal. Validators must preserve this inspection metadata as well as values.
func projectedSensitivity(root *document.Node) map[string]Path { func projectedSensitivity(root *document.Node) map[string]Path {
result := make(map[string]Path) result := make(map[string]Path)
var visit func(*document.Node, Path) walkDocument(root, Path{}, func(path Path, node *document.Node) {
visit = func(node *document.Node, path Path) {
if node == nil {
return
}
if node.Sensitive { if node.Sensitive {
result[path.String()] = path.clone() result[path.String()] = path.clone()
} }
for _, entry := range node.Mapping { })
visit(entry.Value, path.Key(entry.Key))
}
for i, child := range node.Sequence {
visit(child, path.Index(i))
}
}
visit(root, Path{})
return result return result
} }