Retain import inclusion graph during acquisition
This commit is contained in:
@@ -8,6 +8,7 @@ import "gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
|||||||
type composedDocument struct {
|
type composedDocument struct {
|
||||||
root *document.Node
|
root *document.Node
|
||||||
acquisition []acquisitionScope
|
acquisition []acquisitionScope
|
||||||
|
importGraph importInclusionGraph
|
||||||
claims []compositionClaim
|
claims []compositionClaim
|
||||||
sequenceRanges []sequenceRange
|
sequenceRanges []sequenceRange
|
||||||
origins map[string]Origin
|
origins map[string]Origin
|
||||||
@@ -51,6 +52,7 @@ func cloneComposedDocument(base *composedDocument) *composedDocument {
|
|||||||
return &composedDocument{
|
return &composedDocument{
|
||||||
root: document.Clone(base.root), claims: cloneCompositionClaims(base.claims),
|
root: document.Clone(base.root), claims: cloneCompositionClaims(base.claims),
|
||||||
acquisition: cloneAcquisitionScopes(base.acquisition),
|
acquisition: cloneAcquisitionScopes(base.acquisition),
|
||||||
|
importGraph: base.importGraph.clone(),
|
||||||
sequenceRanges: cloneSequenceRanges(base.sequenceRanges),
|
sequenceRanges: cloneSequenceRanges(base.sequenceRanges),
|
||||||
origins: cloneOriginIndex(base.origins), overlays: cloneOverlayRecords(base.overlays),
|
origins: cloneOriginIndex(base.origins), overlays: cloneOverlayRecords(base.overlays),
|
||||||
applications: cloneHistoryApplications(base.applications),
|
applications: cloneHistoryApplications(base.applications),
|
||||||
|
|||||||
190
internal/engine/import_graph.go
Normal file
190
internal/engine/import_graph.go
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
package engine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type importNodeKey struct {
|
||||||
|
namespaceKind string
|
||||||
|
namespaceAnchor string
|
||||||
|
resourceKind string
|
||||||
|
resourcePath string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (key importNodeKey) dependencyKey() dependencyKey {
|
||||||
|
return dependencyKey{key.namespaceKind, key.namespaceAnchor, key.resourceKind, key.resourcePath}
|
||||||
|
}
|
||||||
|
|
||||||
|
type importContributionRole string
|
||||||
|
|
||||||
|
const (
|
||||||
|
importRoleMain importContributionRole = "main"
|
||||||
|
importRoleMapping importContributionRole = "mapping-import"
|
||||||
|
importRoleItem importContributionRole = "item-import"
|
||||||
|
)
|
||||||
|
|
||||||
|
type importGraphNode struct {
|
||||||
|
key importNodeKey
|
||||||
|
canonical string
|
||||||
|
sourceID string
|
||||||
|
role importContributionRole
|
||||||
|
}
|
||||||
|
|
||||||
|
type importInclusionEdge struct {
|
||||||
|
importer importNodeKey
|
||||||
|
imported importNodeKey
|
||||||
|
location Location
|
||||||
|
}
|
||||||
|
|
||||||
|
// importInclusionGraph retains source inclusion independently of mapping-scope
|
||||||
|
// ownership. Scheduling edges point from each imported prerequisite to its
|
||||||
|
// importer, matching the shared dependency scheduler's convention.
|
||||||
|
type importInclusionGraph struct {
|
||||||
|
nodes []importGraphNode
|
||||||
|
edges []importInclusionEdge
|
||||||
|
index map[importNodeKey]int
|
||||||
|
}
|
||||||
|
|
||||||
|
func newImportInclusionGraph() importInclusionGraph {
|
||||||
|
return importInclusionGraph{index: make(map[importNodeKey]int)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (graph *importInclusionGraph) addNode(node importGraphNode) int {
|
||||||
|
if graph.index == nil {
|
||||||
|
graph.index = make(map[importNodeKey]int)
|
||||||
|
}
|
||||||
|
if index, exists := graph.index[node.key]; exists {
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
index := len(graph.nodes)
|
||||||
|
graph.index[node.key] = index
|
||||||
|
graph.nodes = append(graph.nodes, node)
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
|
||||||
|
func (graph *importInclusionGraph) addEdge(importer, imported importNodeKey, location Location) int {
|
||||||
|
index := len(graph.edges)
|
||||||
|
graph.edges = append(graph.edges, importInclusionEdge{
|
||||||
|
importer: importer, imported: imported, location: location,
|
||||||
|
})
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
|
||||||
|
func (graph importInclusionGraph) clone() importInclusionGraph {
|
||||||
|
cloned := importInclusionGraph{
|
||||||
|
nodes: append([]importGraphNode(nil), graph.nodes...),
|
||||||
|
edges: make([]importInclusionEdge, len(graph.edges)),
|
||||||
|
index: make(map[importNodeKey]int, len(graph.index)),
|
||||||
|
}
|
||||||
|
for index, edge := range graph.edges {
|
||||||
|
cloned.edges[index] = importInclusionEdge{
|
||||||
|
importer: edge.importer, imported: edge.imported, location: edge.location,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for key, index := range graph.index {
|
||||||
|
cloned.index[key] = index
|
||||||
|
}
|
||||||
|
return cloned
|
||||||
|
}
|
||||||
|
|
||||||
|
func (graph importInclusionGraph) scheduleCycle(closingEdge int, location Location) (Diagnostic, bool) {
|
||||||
|
keys := make([]dependencyKey, len(graph.nodes))
|
||||||
|
for index, node := range graph.nodes {
|
||||||
|
keys[index] = node.key.dependencyKey()
|
||||||
|
}
|
||||||
|
edges := make([]dependencyEdge, 0, len(graph.edges))
|
||||||
|
for _, edge := range graph.edges {
|
||||||
|
importer, importerOK := graph.index[edge.importer]
|
||||||
|
imported, importedOK := graph.index[edge.imported]
|
||||||
|
if !importerOK || !importedOK {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
edges = append(edges, dependencyEdge{prerequisite: imported, dependent: importer})
|
||||||
|
}
|
||||||
|
_, cycle := scheduleDependencies(keys, edges)
|
||||||
|
if len(cycle) < 2 {
|
||||||
|
return Diagnostic{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scheduler edges reverse inclusion edges. Reverse its closed cycle to
|
||||||
|
// render the authored import route while keeping the canonical start node.
|
||||||
|
route := make([]int, 0, len(cycle))
|
||||||
|
route = append(route, cycle[0])
|
||||||
|
for index := len(cycle) - 2; index > 0; index-- {
|
||||||
|
route = append(route, cycle[index])
|
||||||
|
}
|
||||||
|
route = append(route, cycle[0])
|
||||||
|
paths := make([]string, len(route))
|
||||||
|
for index, node := range route {
|
||||||
|
paths[index] = graph.nodes[node].canonical
|
||||||
|
}
|
||||||
|
|
||||||
|
related := make([]Location, 0, len(route)-2)
|
||||||
|
for index := 0; index+1 < len(route); index++ {
|
||||||
|
importerKey := graph.nodes[route[index]].key
|
||||||
|
importedKey := graph.nodes[route[index+1]].key
|
||||||
|
edgeIndex := -1
|
||||||
|
for candidate, edge := range graph.edges {
|
||||||
|
if edge.importer == importerKey && edge.imported == importedKey {
|
||||||
|
edgeIndex = candidate
|
||||||
|
if candidate == closingEdge {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if edgeIndex == closingEdge {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if edgeIndex >= 0 {
|
||||||
|
related = append(related, graph.edges[edgeIndex].location)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Diagnostic{
|
||||||
|
Code: CodeImportCycle, Severity: SeverityError,
|
||||||
|
Message: "import cycle detected: " + strings.Join(paths, " -> "),
|
||||||
|
Primary: cloneLocation(&location), Related: related,
|
||||||
|
}, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func importKeyFor(access *filesystemAccess, canonical string, synthetic bool) importNodeKey {
|
||||||
|
kind := "host"
|
||||||
|
resourceKind := "file"
|
||||||
|
resourcePath := filepath.ToSlash(canonical)
|
||||||
|
if access.native != nil {
|
||||||
|
kind = "native"
|
||||||
|
}
|
||||||
|
if synthetic {
|
||||||
|
resourceKind = "synthetic"
|
||||||
|
resourcePath = ""
|
||||||
|
}
|
||||||
|
return importNodeKey{
|
||||||
|
namespaceKind: kind,
|
||||||
|
// This traversal owns the main input position; grouped inputs receive
|
||||||
|
// their explicit positions when their acquisition graphs are combined.
|
||||||
|
namespaceAnchor: fmt.Sprintf("%020d/%020d", 0, 0),
|
||||||
|
resourceKind: resourceKind, resourcePath: resourcePath,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func importKeyForMainSource(source *SourceInfo) importNodeKey {
|
||||||
|
if source == nil {
|
||||||
|
return importNodeKey{}
|
||||||
|
}
|
||||||
|
namespaceKind := "host"
|
||||||
|
if source.Kind == SourceKindFilesystem {
|
||||||
|
namespaceKind = "native"
|
||||||
|
}
|
||||||
|
synthetic := source.Kind != SourceKindFile && source.Kind != SourceKindFilesystem
|
||||||
|
resourceKind := "file"
|
||||||
|
resourcePath := filepath.ToSlash(source.Path)
|
||||||
|
if synthetic {
|
||||||
|
resourceKind = "synthetic"
|
||||||
|
resourcePath = ""
|
||||||
|
}
|
||||||
|
return importNodeKey{
|
||||||
|
namespaceKind: namespaceKind, namespaceAnchor: fmt.Sprintf("%020d/%020d", 0, 0),
|
||||||
|
resourceKind: resourceKind, resourcePath: resourcePath,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -35,6 +35,7 @@ func defaultImportLimits() importLimits {
|
|||||||
type importRoute struct {
|
type importRoute struct {
|
||||||
canonical string
|
canonical string
|
||||||
locations []Location
|
locations []Location
|
||||||
|
key importNodeKey
|
||||||
}
|
}
|
||||||
|
|
||||||
type importAccounting struct {
|
type importAccounting struct {
|
||||||
@@ -59,6 +60,7 @@ type importTraversal struct {
|
|||||||
active map[resourceIdentity]int
|
active map[resourceIdentity]int
|
||||||
visited map[resourceIdentity]importRoute
|
visited map[resourceIdentity]importRoute
|
||||||
stack []importRoute
|
stack []importRoute
|
||||||
|
importGraph importInclusionGraph
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadImportedDocuments(source Source, context *FilesystemContext, main parsedSource, rule *VersionRule, limits importLimits) (*composedDocument, []SourceInfo, []Diagnostic) {
|
func loadImportedDocuments(source Source, context *FilesystemContext, main parsedSource, rule *VersionRule, limits importLimits) (*composedDocument, []SourceInfo, []Diagnostic) {
|
||||||
@@ -139,11 +141,12 @@ func loadImportedWithAccessTrackedAndProcessingPolicy(access *filesystemAccess,
|
|||||||
if mainPath != "" {
|
if mainPath != "" {
|
||||||
mainInfo.Path = mainPath
|
mainInfo.Path = mainPath
|
||||||
}
|
}
|
||||||
mainKey := access.identity(mainPath)
|
mainResource := access.identity(mainPath)
|
||||||
|
mainNodeKey := importKeyFor(access, mainPath, mainPath == "")
|
||||||
if mainPath == "" {
|
if mainPath == "" {
|
||||||
mainKey = access.syntheticIdentity()
|
mainResource = access.syntheticIdentity()
|
||||||
}
|
}
|
||||||
if previous, repeated := visited[mainKey]; repeated {
|
if previous, repeated := visited[mainResource]; repeated {
|
||||||
_ = access.close()
|
_ = access.close()
|
||||||
return nil, sourceInfos(main.source), []Diagnostic{importRepeatedDiagnostic(Location{SourceID: main.source.ID}, previous, importRoute{})}
|
return nil, sourceInfos(main.source), []Diagnostic{importRepeatedDiagnostic(Location{SourceID: main.source.ID}, previous, importRoute{})}
|
||||||
}
|
}
|
||||||
@@ -152,9 +155,10 @@ func loadImportedWithAccessTrackedAndProcessingPolicy(access *filesystemAccess,
|
|||||||
accounting.nodes += countInputNodes(main.document)
|
accounting.nodes += countInputNodes(main.document)
|
||||||
state := importTraversal{
|
state := importTraversal{
|
||||||
access: access, accounting: accounting, limits: limits, rule: rule, versionState: versionState,
|
access: access, accounting: accounting, limits: limits, rule: rule, versionState: versionState,
|
||||||
scope: acquisitionScope{destination: destination.clone()},
|
scope: acquisitionScope{owner: mainNodeKey, destination: destination.clone()},
|
||||||
sources: []SourceInfo{mainInfo},
|
sources: []SourceInfo{mainInfo},
|
||||||
active: make(map[resourceIdentity]int), visited: visited,
|
active: make(map[resourceIdentity]int), visited: visited,
|
||||||
|
importGraph: newImportInclusionGraph(),
|
||||||
}
|
}
|
||||||
if mainIsItem {
|
if mainIsItem {
|
||||||
state.itemScope = true
|
state.itemScope = true
|
||||||
@@ -176,8 +180,10 @@ func loadImportedWithAccessTrackedAndProcessingPolicy(access *filesystemAccess,
|
|||||||
mainIdentity = "<selected-source>"
|
mainIdentity = "<selected-source>"
|
||||||
}
|
}
|
||||||
mainRoute := importRoute{canonical: mainIdentity}
|
mainRoute := importRoute{canonical: mainIdentity}
|
||||||
state.active[mainKey] = 0
|
state.importGraph.addNode(importGraphNode{key: mainNodeKey, canonical: mainIdentity, sourceID: mainInfo.ID, role: importRoleMain})
|
||||||
state.visited[mainKey] = mainRoute
|
mainRoute.key = mainNodeKey
|
||||||
|
state.active[mainResource] = 0
|
||||||
|
state.visited[mainResource] = mainRoute
|
||||||
state.stack = append(state.stack, mainRoute)
|
state.stack = append(state.stack, mainRoute)
|
||||||
var body *document.Node
|
var body *document.Node
|
||||||
var declarations []importDeclaration
|
var declarations []importDeclaration
|
||||||
@@ -191,7 +197,8 @@ func loadImportedWithAccessTrackedAndProcessingPolicy(access *filesystemAccess,
|
|||||||
if len(diagnostics) == 0 {
|
if len(diagnostics) == 0 {
|
||||||
if mainIsItem {
|
if mainIsItem {
|
||||||
state.scope.contributions = append(state.scope.contributions, acquiredContribution{
|
state.scope.contributions = append(state.scope.contributions, acquiredContribution{
|
||||||
source: cloneSourceInfo(mainInfo), payload: body, document: body, processing: processing,
|
source: cloneSourceInfo(mainInfo), identity: mainNodeKey, role: importRoleMain,
|
||||||
|
scopeOwner: mainNodeKey, payload: body, document: body, processing: processing,
|
||||||
})
|
})
|
||||||
diagnostics = state.walkDeclarations(basePath, declarations, 0, Path{})
|
diagnostics = state.walkDeclarations(basePath, declarations, 0, Path{})
|
||||||
} else {
|
} else {
|
||||||
@@ -205,12 +212,16 @@ func loadImportedWithAccessTrackedAndProcessingPolicy(access *filesystemAccess,
|
|||||||
accounting.nodes += placementNodes
|
accounting.nodes += placementNodes
|
||||||
state.scope.contributions = append(state.scope.contributions, acquiredContribution{
|
state.scope.contributions = append(state.scope.contributions, acquiredContribution{
|
||||||
source: cloneSourceInfo(mainInfo), payload: body, document: placed,
|
source: cloneSourceInfo(mainInfo), payload: body, document: placed,
|
||||||
|
identity: mainNodeKey, role: importRoleMain, scopeOwner: mainNodeKey,
|
||||||
destination: destination.clone(), processing: processing,
|
destination: destination.clone(), processing: processing,
|
||||||
})
|
})
|
||||||
diagnostics = state.walkDeclarations(basePath, declarations, 0, destination)
|
diagnostics = state.walkDeclarations(basePath, declarations, 0, destination)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if len(diagnostics) == 0 {
|
||||||
|
diagnostics = state.composePendingItems(&state.scope)
|
||||||
|
}
|
||||||
|
|
||||||
closeErr := access.close()
|
closeErr := access.close()
|
||||||
if len(diagnostics) != 0 {
|
if len(diagnostics) != 0 {
|
||||||
@@ -267,13 +278,16 @@ func loadImportedWithAccessTrackedAndProcessingPolicy(access *filesystemAccess,
|
|||||||
if composed != nil {
|
if composed != nil {
|
||||||
synthetic := acquisitionScope{contributions: []acquiredContribution{{
|
synthetic := acquisitionScope{contributions: []acquiredContribution{{
|
||||||
source: cloneSourceInfo(mainInfo), payload: completed.root, document: placed,
|
source: cloneSourceInfo(mainInfo), payload: completed.root, document: placed,
|
||||||
|
identity: mainNodeKey, role: importRoleMain, scopeOwner: mainNodeKey,
|
||||||
destination: destination.clone(), processing: processing, nested: completed,
|
destination: destination.clone(), processing: processing, nested: completed,
|
||||||
}}}
|
}}}
|
||||||
|
synthetic.owner = mainNodeKey
|
||||||
projectNestedEvidence(composed, synthetic)
|
projectNestedEvidence(composed, synthetic)
|
||||||
projectAcquiredDestinations(composed, synthetic, state.sources)
|
projectAcquiredDestinations(composed, synthetic, state.sources)
|
||||||
composed.acquisition = []acquisitionScope{cloneAcquisitionScope(synthetic)}
|
composed.acquisition = []acquisitionScope{cloneAcquisitionScope(synthetic)}
|
||||||
composed.inputBytes = accounting.bytes
|
composed.inputBytes = accounting.bytes
|
||||||
composed.inputNodes = accounting.nodes
|
composed.inputNodes = accounting.nodes
|
||||||
|
composed.importGraph = state.importGraph.clone()
|
||||||
}
|
}
|
||||||
return composed, cloneSourceInfos(state.sources), failures
|
return composed, cloneSourceInfos(state.sources), failures
|
||||||
}
|
}
|
||||||
@@ -293,6 +307,7 @@ func loadImportedWithAccessTrackedAndProcessingPolicy(access *filesystemAccess,
|
|||||||
composed.acquisition = []acquisitionScope{cloneAcquisitionScope(state.scope)}
|
composed.acquisition = []acquisitionScope{cloneAcquisitionScope(state.scope)}
|
||||||
composed.inputBytes = accounting.bytes
|
composed.inputBytes = accounting.bytes
|
||||||
composed.inputNodes = accounting.nodes
|
composed.inputNodes = accounting.nodes
|
||||||
|
composed.importGraph = state.importGraph.clone()
|
||||||
}
|
}
|
||||||
return composed, cloneSourceInfos(state.sources), diagnostics
|
return composed, cloneSourceInfos(state.sources), diagnostics
|
||||||
}
|
}
|
||||||
@@ -386,11 +401,18 @@ func (s *importTraversal) visitPath(basePath string, declaration importDeclarati
|
|||||||
|
|
||||||
currentRoute := s.currentRoute(location)
|
currentRoute := s.currentRoute(location)
|
||||||
identity := s.access.identity(canonical)
|
identity := s.access.identity(canonical)
|
||||||
|
nodeKey := importKeyFor(s.access, canonical, false)
|
||||||
|
importerKey := s.currentImportNode()
|
||||||
if activeIndex, ok := s.active[identity]; ok {
|
if activeIndex, ok := s.active[identity]; ok {
|
||||||
|
closingEdge := s.importGraph.addEdge(importerKey, nodeKey, location)
|
||||||
|
if diagnostic, ok := s.importGraph.scheduleCycle(closingEdge, location); ok {
|
||||||
|
return []Diagnostic{diagnostic}
|
||||||
|
}
|
||||||
cycle := append([]importRoute(nil), s.stack[activeIndex:]...)
|
cycle := append([]importRoute(nil), s.stack[activeIndex:]...)
|
||||||
return []Diagnostic{importCycleDiagnostic(canonical, location, cycle)}
|
return []Diagnostic{importCycleDiagnostic(canonical, location, cycle)}
|
||||||
}
|
}
|
||||||
if previous, ok := s.visited[identity]; ok {
|
if previous, ok := s.visited[identity]; ok {
|
||||||
|
s.importGraph.addEdge(importerKey, nodeKey, location)
|
||||||
return []Diagnostic{importRepeatedDiagnostic(location, previous, currentRoute)}
|
return []Diagnostic{importRepeatedDiagnostic(location, previous, currentRoute)}
|
||||||
}
|
}
|
||||||
if s.accounting.sources+1 > s.limits.sources {
|
if s.accounting.sources+1 > s.limits.sources {
|
||||||
@@ -467,12 +489,18 @@ func (s *importTraversal) visitPath(basePath string, declaration importDeclarati
|
|||||||
if declaration.placement == placementModeSequenceItem {
|
if declaration.placement == placementModeSequenceItem {
|
||||||
mode = placementModeSequenceItem
|
mode = placementModeSequenceItem
|
||||||
}
|
}
|
||||||
|
role := importRoleMapping
|
||||||
if mode == placementModeSequenceItem {
|
if mode == placementModeSequenceItem {
|
||||||
route := importRoute{canonical: canonical, locations: currentRoute.locations}
|
role = importRoleItem
|
||||||
|
}
|
||||||
|
s.importGraph.addNode(importGraphNode{key: nodeKey, canonical: canonical, sourceID: sourceID, role: role})
|
||||||
|
s.importGraph.addEdge(importerKey, nodeKey, location)
|
||||||
|
if mode == placementModeSequenceItem {
|
||||||
|
route := importRoute{canonical: canonical, locations: currentRoute.locations, key: nodeKey}
|
||||||
s.active[identity] = len(s.stack)
|
s.active[identity] = len(s.stack)
|
||||||
s.visited[identity] = route
|
s.visited[identity] = route
|
||||||
s.stack = append(s.stack, route)
|
s.stack = append(s.stack, route)
|
||||||
diagnostics := s.visitItemSource(canonical, declaration, destination, relativeDestination, depth, info, body, declarations, processing)
|
diagnostics := s.visitItemSource(canonical, nodeKey, declaration, destination, relativeDestination, depth, info, body, declarations, processing)
|
||||||
delete(s.active, identity)
|
delete(s.active, identity)
|
||||||
s.stack = s.stack[:len(s.stack)-1]
|
s.stack = s.stack[:len(s.stack)-1]
|
||||||
return diagnostics
|
return diagnostics
|
||||||
@@ -486,9 +514,10 @@ func (s *importTraversal) visitPath(basePath string, declaration importDeclarati
|
|||||||
return []Diagnostic{importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the aggregate node limit", &location)}
|
return []Diagnostic{importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the aggregate node limit", &location)}
|
||||||
}
|
}
|
||||||
s.accounting.nodes += placementNodes
|
s.accounting.nodes += placementNodes
|
||||||
route := importRoute{canonical: canonical, locations: currentRoute.locations}
|
route := importRoute{canonical: canonical, locations: currentRoute.locations, key: nodeKey}
|
||||||
s.scope.contributions = append(s.scope.contributions, acquiredContribution{
|
s.scope.contributions = append(s.scope.contributions, acquiredContribution{
|
||||||
source: cloneSourceInfo(info), payload: body, document: placed,
|
source: cloneSourceInfo(info), payload: body, document: placed,
|
||||||
|
identity: nodeKey, role: role, scopeOwner: s.scope.owner,
|
||||||
destination: placementDestination.clone(), relativeDestination: relativeDestination.clone(), processing: processing,
|
destination: placementDestination.clone(), relativeDestination: relativeDestination.clone(), processing: processing,
|
||||||
})
|
})
|
||||||
s.active[identity] = len(s.stack)
|
s.active[identity] = len(s.stack)
|
||||||
@@ -500,6 +529,17 @@ func (s *importTraversal) visitPath(basePath string, declaration importDeclarati
|
|||||||
return diagnostics
|
return diagnostics
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *importTraversal) currentImportNode() importNodeKey {
|
||||||
|
if len(s.stack) != 0 {
|
||||||
|
return s.stack[len(s.stack)-1].key
|
||||||
|
}
|
||||||
|
key := s.scope.owner
|
||||||
|
if _, exists := s.importGraph.index[key]; !exists {
|
||||||
|
s.importGraph.addNode(importGraphNode{key: key, canonical: "<selected-source>", role: importRoleMain})
|
||||||
|
}
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
func (s *importTraversal) sourceDestination(destination Path) Path {
|
func (s *importTraversal) sourceDestination(destination Path) Path {
|
||||||
if !s.itemScope {
|
if !s.itemScope {
|
||||||
return destination.clone()
|
return destination.clone()
|
||||||
@@ -509,10 +549,10 @@ func (s *importTraversal) sourceDestination(destination Path) Path {
|
|||||||
return s.itemFallback.clone()
|
return s.itemFallback.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *importTraversal) visitItemSource(canonical string, declaration importDeclaration, destination, relativeDestination Path, depth int, info SourceInfo, body *document.Node, declarations []importDeclaration, processing *sourceVersionProcessingResult) []Diagnostic {
|
func (s *importTraversal) visitItemSource(canonical string, identity importNodeKey, declaration importDeclaration, destination, relativeDestination Path, depth int, info SourceInfo, body *document.Node, declarations []importDeclaration, processing *sourceVersionProcessingResult) []Diagnostic {
|
||||||
parentScope := s.scope
|
parentScope := s.scope
|
||||||
parentItemScope, parentPrefix, parentFallback := s.itemScope, s.itemPrefix, s.itemFallback
|
parentItemScope, parentPrefix, parentFallback := s.itemScope, s.itemPrefix, s.itemFallback
|
||||||
s.scope = acquisitionScope{}
|
s.scope = acquisitionScope{owner: identity}
|
||||||
s.itemScope = true
|
s.itemScope = true
|
||||||
if parentItemScope {
|
if parentItemScope {
|
||||||
s.itemPrefix = appendDestination(parentPrefix, destination)
|
s.itemPrefix = appendDestination(parentPrefix, destination)
|
||||||
@@ -522,6 +562,7 @@ func (s *importTraversal) visitItemSource(canonical string, declaration importDe
|
|||||||
}
|
}
|
||||||
s.scope.contributions = append(s.scope.contributions, acquiredContribution{
|
s.scope.contributions = append(s.scope.contributions, acquiredContribution{
|
||||||
source: cloneSourceInfo(info), payload: body, document: body,
|
source: cloneSourceInfo(info), payload: body, document: body,
|
||||||
|
identity: identity, role: importRoleItem, scopeOwner: identity,
|
||||||
processing: processing,
|
processing: processing,
|
||||||
})
|
})
|
||||||
diagnostics := s.walkDeclarations(s.access.dir(canonical), declarations, depth, Path{})
|
diagnostics := s.walkDeclarations(s.access.dir(canonical), declarations, depth, Path{})
|
||||||
@@ -530,35 +571,53 @@ func (s *importTraversal) visitItemSource(canonical string, declaration importDe
|
|||||||
if len(diagnostics) != 0 {
|
if len(diagnostics) != 0 {
|
||||||
return projectItemDiagnosticPaths(diagnostics, parentScope, destination)
|
return projectItemDiagnosticPaths(diagnostics, parentScope, destination)
|
||||||
}
|
}
|
||||||
documents := make([]*document.Node, 0, len(itemScope.contributions))
|
|
||||||
for _, contribution := range itemScope.contributions {
|
|
||||||
documents = append(documents, contribution.document)
|
|
||||||
}
|
|
||||||
completed, diagnostics := composeDocuments(documents...)
|
|
||||||
if len(diagnostics) != 0 {
|
|
||||||
return projectItemDiagnosticPaths(diagnostics, parentScope, destination)
|
|
||||||
}
|
|
||||||
itemScope.destination = Path{}
|
itemScope.destination = Path{}
|
||||||
completed.acquisition = []acquisitionScope{cloneAcquisitionScope(itemScope)}
|
|
||||||
projectNestedEvidence(completed, itemScope)
|
|
||||||
pl := destination
|
|
||||||
_, placed, err := placeDocumentWithMode(completed.root, pl, placementModeSequenceItem, body.Location, maxDocumentDepth, maxDocumentNodes)
|
|
||||||
if err != nil {
|
|
||||||
return []Diagnostic{importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the source depth or node limit after destination placement", &declaration.location)}
|
|
||||||
}
|
|
||||||
placementNodes := destinationWrapperNodeCountForMode(pl, placementModeSequenceItem)
|
|
||||||
if s.accounting.nodes+placementNodes > s.limits.nodes {
|
|
||||||
return []Diagnostic{importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the aggregate node limit", &declaration.location)}
|
|
||||||
}
|
|
||||||
s.accounting.nodes += placementNodes
|
|
||||||
s.scope.contributions = append(s.scope.contributions, acquiredContribution{
|
s.scope.contributions = append(s.scope.contributions, acquiredContribution{
|
||||||
source: cloneSourceInfo(info), payload: completed.root, document: placed,
|
source: cloneSourceInfo(info), payload: body,
|
||||||
destination: pl.clone(), relativeDestination: relativeDestination.clone(), processing: processing,
|
identity: identity, role: importRoleItem, scopeOwner: parentScope.owner,
|
||||||
nested: completed,
|
destination: destination.clone(), relativeDestination: relativeDestination.clone(), processing: processing,
|
||||||
|
pendingItem: &itemScope,
|
||||||
})
|
})
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *importTraversal) composePendingItems(scope *acquisitionScope) []Diagnostic {
|
||||||
|
for index := range scope.contributions {
|
||||||
|
contribution := &scope.contributions[index]
|
||||||
|
if contribution.pendingItem == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
itemScope := contribution.pendingItem
|
||||||
|
if diagnostics := s.composePendingItems(itemScope); len(diagnostics) != 0 {
|
||||||
|
return projectItemDiagnosticPaths(diagnostics, *scope, contribution.destination)
|
||||||
|
}
|
||||||
|
documents := make([]*document.Node, 0, len(itemScope.contributions))
|
||||||
|
for _, nested := range itemScope.contributions {
|
||||||
|
documents = append(documents, nested.document)
|
||||||
|
}
|
||||||
|
completed, diagnostics := composeDocuments(documents...)
|
||||||
|
if len(diagnostics) != 0 {
|
||||||
|
return projectItemDiagnosticPaths(diagnostics, *scope, contribution.destination)
|
||||||
|
}
|
||||||
|
completed.acquisition = []acquisitionScope{cloneAcquisitionScope(*itemScope)}
|
||||||
|
projectNestedEvidence(completed, *itemScope)
|
||||||
|
_, placed, err := placeDocumentWithMode(completed.root, contribution.destination, placementModeSequenceItem, contribution.payload.Location, maxDocumentDepth, maxDocumentNodes)
|
||||||
|
if err != nil {
|
||||||
|
return []Diagnostic{importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the source depth or node limit after destination placement", contribution.source.ImportedFrom)}
|
||||||
|
}
|
||||||
|
placementNodes := destinationWrapperNodeCountForMode(contribution.destination, placementModeSequenceItem)
|
||||||
|
if s.accounting.nodes+placementNodes > s.limits.nodes {
|
||||||
|
return []Diagnostic{importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the aggregate node limit", contribution.source.ImportedFrom)}
|
||||||
|
}
|
||||||
|
s.accounting.nodes += placementNodes
|
||||||
|
contribution.payload = completed.root
|
||||||
|
contribution.document = placed
|
||||||
|
contribution.nested = completed
|
||||||
|
contribution.pendingItem = nil
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func importedVersionClaim(documents []*document.Node, composed *composedDocument, rule *VersionRule, mainIndex int) *Diagnostic {
|
func importedVersionClaim(documents []*document.Node, composed *composedDocument, rule *VersionRule, mainIndex int) *Diagnostic {
|
||||||
if composed == nil || rule == nil {
|
if composed == nil || rule == nil {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -656,6 +656,22 @@ func TestLoadImportIdentityReportsCyclesAndRepeatedRoutes(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("cycle through an item scope", func(t *testing.T) {
|
||||||
|
directory := t.TempDir()
|
||||||
|
mainPath := writeImportFile(t, directory, "main.yml", `configkit:
|
||||||
|
imports:
|
||||||
|
- path: item.yml
|
||||||
|
destination: [items]
|
||||||
|
placement: sequence-item
|
||||||
|
`)
|
||||||
|
writeImportFile(t, directory, "item.yml", "configkit: {imports: [{path: main.yml}]}\n")
|
||||||
|
loadError := requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, CodeImportCycle)
|
||||||
|
diagnostic := loadError.Diagnostics()[0]
|
||||||
|
if diagnostic.Primary == nil || len(diagnostic.Related) != 1 || !strings.Contains(diagnostic.Message, "main.yml") || !strings.Contains(diagnostic.Message, "item.yml") {
|
||||||
|
t.Fatalf("nested item cycle diagnostic = %#v", diagnostic)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("diamond", func(t *testing.T) {
|
t.Run("diamond", func(t *testing.T) {
|
||||||
directory := t.TempDir()
|
directory := t.TempDir()
|
||||||
mainPath := writeImportFile(t, directory, "main.yml", "configkit:\n imports: [{path: left.yml}, {path: right.yml}]\n")
|
mainPath := writeImportFile(t, directory, "main.yml", "configkit:\n imports: [{path: left.yml}, {path: right.yml}]\n")
|
||||||
@@ -686,6 +702,61 @@ func TestLoadImportIdentityReportsCyclesAndRepeatedRoutes(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestItemAcquisitionRetainsInclusionGraphAndScopeOwnership(t *testing.T) {
|
||||||
|
directory := t.TempDir()
|
||||||
|
mainPath := writeImportFile(t, directory, "main.yml", `configkit:
|
||||||
|
imports:
|
||||||
|
- path: item.yml
|
||||||
|
destination: [items]
|
||||||
|
placement: sequence-item
|
||||||
|
`)
|
||||||
|
itemPath := writeImportFile(t, directory, "item.yml", "configkit: {imports: [{path: helper.yml}]}\nname: item\n")
|
||||||
|
helperPath := writeImportFile(t, directory, "helper.yml", "id: supplied\n")
|
||||||
|
|
||||||
|
input, diagnostics := assembleBaseInput[importTestConfig](File(mainPath), Options[importTestConfig]{Imports: true})
|
||||||
|
if len(diagnostics) != 0 {
|
||||||
|
t.Fatalf("assemble input: %#v", diagnostics)
|
||||||
|
}
|
||||||
|
graph := input.composition.importGraph
|
||||||
|
if len(graph.nodes) != 3 || len(graph.edges) != 2 {
|
||||||
|
t.Fatalf("inclusion graph has %d nodes and %d edges, want 3 and 2", len(graph.nodes), len(graph.edges))
|
||||||
|
}
|
||||||
|
for index, want := range []struct {
|
||||||
|
path string
|
||||||
|
role importContributionRole
|
||||||
|
}{
|
||||||
|
{path: canonicalTestPath(t, mainPath), role: importRoleMain},
|
||||||
|
{path: canonicalTestPath(t, itemPath), role: importRoleItem},
|
||||||
|
{path: canonicalTestPath(t, helperPath), role: importRoleMapping},
|
||||||
|
} {
|
||||||
|
node := graph.nodes[index]
|
||||||
|
if node.canonical != want.path || node.role != want.role || node.sourceID != "source-"+strconv.Itoa(index+1) {
|
||||||
|
t.Errorf("graph node %d = %#v, want %s, %q, source-%d", index, node, want.role, want.path, index+1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rootScope := input.composition.acquisition[0]
|
||||||
|
if rootScope.owner != graph.nodes[0].key || len(rootScope.contributions) != 2 {
|
||||||
|
t.Fatalf("root acquisition scope = %#v", rootScope)
|
||||||
|
}
|
||||||
|
item := rootScope.contributions[1]
|
||||||
|
if item.identity != graph.nodes[1].key || item.role != importRoleItem || item.scopeOwner != rootScope.owner || item.nested == nil {
|
||||||
|
t.Fatalf("outer item contribution = %#v", item)
|
||||||
|
}
|
||||||
|
itemScope := item.nested.acquisition[0]
|
||||||
|
if itemScope.owner != item.identity || len(itemScope.contributions) != 2 {
|
||||||
|
t.Fatalf("nested item scope = %#v", itemScope)
|
||||||
|
}
|
||||||
|
for _, contribution := range itemScope.contributions {
|
||||||
|
if contribution.scopeOwner != item.identity {
|
||||||
|
t.Errorf("nested contribution scope owner = %#v, want item %v", contribution.scopeOwner, item.identity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if graph.edges[0].importer != graph.nodes[0].key || graph.edges[0].imported != graph.nodes[1].key || graph.edges[0].location.SourceID != "source-1" ||
|
||||||
|
graph.edges[1].importer != graph.nodes[1].key || graph.edges[1].imported != graph.nodes[2].key || graph.edges[1].location.SourceID != "source-2" {
|
||||||
|
t.Fatalf("inclusion edges = %#v", graph.edges)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestLoadImportsCanonicalSymlinksAndKeepsHardLinksDistinct(t *testing.T) {
|
func TestLoadImportsCanonicalSymlinksAndKeepsHardLinksDistinct(t *testing.T) {
|
||||||
if runtime.GOOS != "linux" && runtime.GOOS != "darwin" {
|
if runtime.GOOS != "linux" && runtime.GOOS != "darwin" {
|
||||||
t.Skip("initial filesystem contract covers Linux and macOS")
|
t.Skip("initial filesystem contract covers Linux and macOS")
|
||||||
|
|||||||
@@ -23,37 +23,51 @@ type assembledInput struct {
|
|||||||
// boundary for later scoped assembly and evidence.
|
// boundary for later scoped assembly and evidence.
|
||||||
type acquiredContribution struct {
|
type acquiredContribution struct {
|
||||||
source SourceInfo
|
source SourceInfo
|
||||||
|
identity importNodeKey
|
||||||
|
role importContributionRole
|
||||||
|
scopeOwner importNodeKey
|
||||||
payload *document.Node
|
payload *document.Node
|
||||||
document *document.Node
|
document *document.Node
|
||||||
destination Path
|
destination Path
|
||||||
relativeDestination Path
|
relativeDestination Path
|
||||||
processing *sourceVersionProcessingResult
|
processing *sourceVersionProcessingResult
|
||||||
nested *composedDocument
|
nested *composedDocument
|
||||||
|
pendingItem *acquisitionScope
|
||||||
}
|
}
|
||||||
|
|
||||||
// acquisitionScope is the ordered strict mapping scope produced by one input
|
// acquisitionScope is the ordered strict mapping scope produced by one input
|
||||||
// and its ordinary imports.
|
// and its ordinary imports.
|
||||||
type acquisitionScope struct {
|
type acquisitionScope struct {
|
||||||
|
owner importNodeKey
|
||||||
destination Path
|
destination Path
|
||||||
contributions []acquiredContribution
|
contributions []acquiredContribution
|
||||||
}
|
}
|
||||||
|
|
||||||
func cloneAcquisitionScope(scope acquisitionScope) acquisitionScope {
|
func cloneAcquisitionScope(scope acquisitionScope) acquisitionScope {
|
||||||
cloned := acquisitionScope{destination: scope.destination.clone()}
|
cloned := acquisitionScope{owner: scope.owner, destination: scope.destination.clone()}
|
||||||
if scope.contributions != nil {
|
if scope.contributions != nil {
|
||||||
cloned.contributions = make([]acquiredContribution, len(scope.contributions))
|
cloned.contributions = make([]acquiredContribution, len(scope.contributions))
|
||||||
for index, contribution := range scope.contributions {
|
for index, contribution := range scope.contributions {
|
||||||
cloned.contributions[index] = acquiredContribution{
|
cloned.contributions[index] = acquiredContribution{
|
||||||
source: cloneSourceInfo(contribution.source), payload: contribution.payload,
|
source: cloneSourceInfo(contribution.source), identity: contribution.identity,
|
||||||
|
role: contribution.role, scopeOwner: contribution.scopeOwner, payload: contribution.payload,
|
||||||
document: contribution.document, destination: contribution.destination.clone(),
|
document: contribution.document, destination: contribution.destination.clone(),
|
||||||
relativeDestination: contribution.relativeDestination.clone(), processing: contribution.processing,
|
relativeDestination: contribution.relativeDestination.clone(), processing: contribution.processing,
|
||||||
nested: cloneComposedDocument(contribution.nested),
|
nested: cloneComposedDocument(contribution.nested), pendingItem: cloneAcquisitionScopePointer(contribution.pendingItem),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return cloned
|
return cloned
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func cloneAcquisitionScopePointer(scope *acquisitionScope) *acquisitionScope {
|
||||||
|
if scope == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
cloned := cloneAcquisitionScope(*scope)
|
||||||
|
return &cloned
|
||||||
|
}
|
||||||
|
|
||||||
func cloneAcquisitionScopes(scopes []acquisitionScope) []acquisitionScope {
|
func cloneAcquisitionScopes(scopes []acquisitionScope) []acquisitionScope {
|
||||||
if scopes == nil {
|
if scopes == nil {
|
||||||
return nil
|
return nil
|
||||||
@@ -145,10 +159,13 @@ func assembleBaseInputTrackedForRole[T any](source Source, options Options[T], d
|
|||||||
}
|
}
|
||||||
localPayload := input.document
|
localPayload := input.document
|
||||||
input.document = placed
|
input.document = placed
|
||||||
|
identity := importKeyForMainSource(parsed.source)
|
||||||
input.acquisition = []acquisitionScope{{
|
input.acquisition = []acquisitionScope{{
|
||||||
|
owner: identity,
|
||||||
destination: destination.clone(),
|
destination: destination.clone(),
|
||||||
contributions: []acquiredContribution{{
|
contributions: []acquiredContribution{{
|
||||||
source: cloneSourceInfo(*parsed.source), payload: localPayload,
|
source: cloneSourceInfo(*parsed.source), payload: localPayload,
|
||||||
|
identity: identity, role: importRoleMain, scopeOwner: identity,
|
||||||
document: placed, destination: destination.clone(), processing: versionProcessing,
|
document: placed, destination: destination.clone(), processing: versionProcessing,
|
||||||
}},
|
}},
|
||||||
}}
|
}}
|
||||||
|
|||||||
Reference in New Issue
Block a user