Files
ConfigKit/internal/engine/import_graph.go

191 lines
5.4 KiB
Go

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,
}
}