339 lines
10 KiB
Go
339 lines
10 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 mergeImportInclusionGraph(target *importInclusionGraph, source importInclusionGraph) {
|
|
for _, node := range source.nodes {
|
|
target.addNode(node)
|
|
}
|
|
for _, edge := range source.edges {
|
|
target.addEdge(edge.importer, edge.imported, edge.location)
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// importDependencyIndex is built once per scheduling operation and shared by
|
|
// its scopes. Inclusion targets are grouped by importer, so local scheduling
|
|
// need not scan or reserve storage for unrelated graph edges.
|
|
type importDependencyIndex map[importNodeKey][]importNodeKey
|
|
|
|
func indexImportDependencies(graph importInclusionGraph) importDependencyIndex {
|
|
index := make(importDependencyIndex)
|
|
for _, edge := range graph.edges {
|
|
index[edge.importer] = append(index[edge.importer], edge.imported)
|
|
}
|
|
return index
|
|
}
|
|
|
|
func scheduleAcquisitionScope(scope acquisitionScope, dependencies importDependencyIndex) acquisitionScope {
|
|
slots := make([]int, 0, len(scope.contributions))
|
|
for index, contribution := range scope.contributions {
|
|
if contribution.scopeOwner != scope.owner {
|
|
continue
|
|
}
|
|
slots = append(slots, index)
|
|
}
|
|
scheduled := scope
|
|
scheduled.contributions = scheduleContributionSlots(scope.contributions, slots, dependencies)
|
|
return scheduled
|
|
}
|
|
|
|
func scheduleAcquisitionGroup(scopes []acquisitionScope, graph importInclusionGraph) acquisitionScope {
|
|
var contributions []acquiredContribution
|
|
for _, scope := range scopes {
|
|
contributions = append(contributions, scope.contributions...)
|
|
}
|
|
slots := make([]int, len(contributions))
|
|
for index := range slots {
|
|
slots[index] = index
|
|
}
|
|
return acquisitionScope{contributions: scheduleContributionSlots(contributions, slots, indexImportDependencies(graph))}
|
|
}
|
|
|
|
func scheduleContributionSlots(contributions []acquiredContribution, slots []int, dependencies importDependencyIndex) []acquiredContribution {
|
|
if len(slots) < 2 {
|
|
return append([]acquiredContribution(nil), contributions...)
|
|
}
|
|
positions := make(map[importNodeKey]int, len(slots))
|
|
keys := make([]dependencyKey, len(slots))
|
|
for position, slot := range slots {
|
|
contribution := contributions[slot]
|
|
positions[contribution.identity] = position
|
|
keys[position] = contribution.identity.dependencyKey()
|
|
}
|
|
var edges []dependencyEdge
|
|
for importerKey, importer := range positions {
|
|
for _, importedKey := range dependencies[importerKey] {
|
|
if imported, exists := positions[importedKey]; exists {
|
|
edges = append(edges, dependencyEdge{prerequisite: imported, dependent: importer})
|
|
}
|
|
}
|
|
}
|
|
order, _ := scheduleDependencies(keys, edges)
|
|
if len(order) != len(keys) {
|
|
return append([]acquiredContribution(nil), contributions...)
|
|
}
|
|
scheduled := append([]acquiredContribution(nil), contributions...)
|
|
for position, scheduledIndex := range order {
|
|
scheduled[slots[position]] = contributions[slots[scheduledIndex]]
|
|
}
|
|
return scheduled
|
|
}
|
|
|
|
type importNamespaceAnchors struct {
|
|
kind string
|
|
host string
|
|
native string
|
|
input string
|
|
}
|
|
|
|
func explicitNamespaceAnchor(group, input int) string {
|
|
return fmt.Sprintf("%020d/%020d", group, input)
|
|
}
|
|
|
|
func reanchorImportKey(key importNodeKey, anchors importNamespaceAnchors) importNodeKey {
|
|
if anchors.kind != "" {
|
|
key.namespaceKind = anchors.kind
|
|
}
|
|
switch {
|
|
case key.resourceKind == "synthetic":
|
|
key.namespaceAnchor = anchors.input
|
|
case key.namespaceKind == "native":
|
|
key.namespaceAnchor = anchors.native
|
|
default:
|
|
key.namespaceAnchor = anchors.host
|
|
}
|
|
return key
|
|
}
|
|
|
|
func reanchorImportGraph(graph importInclusionGraph, anchors importNamespaceAnchors) (importInclusionGraph, map[importNodeKey]importNodeKey) {
|
|
reanchored := newImportInclusionGraph()
|
|
keys := make(map[importNodeKey]importNodeKey, len(graph.nodes))
|
|
for _, node := range graph.nodes {
|
|
old := node.key
|
|
node.key = reanchorImportKey(old, anchors)
|
|
keys[old] = node.key
|
|
reanchored.addNode(node)
|
|
}
|
|
for _, edge := range graph.edges {
|
|
reanchored.addEdge(keys[edge.importer], keys[edge.imported], edge.location)
|
|
}
|
|
return reanchored, keys
|
|
}
|
|
|
|
func reanchorAcquisitionScope(scope *acquisitionScope, keys map[importNodeKey]importNodeKey, anchors importNamespaceAnchors) {
|
|
if scope == nil {
|
|
return
|
|
}
|
|
if key, exists := keys[scope.owner]; exists {
|
|
scope.owner = key
|
|
} else {
|
|
scope.owner = reanchorImportKey(scope.owner, anchors)
|
|
}
|
|
for index := range scope.contributions {
|
|
contribution := &scope.contributions[index]
|
|
if key, exists := keys[contribution.identity]; exists {
|
|
contribution.identity = key
|
|
} else {
|
|
contribution.identity = reanchorImportKey(contribution.identity, anchors)
|
|
}
|
|
if key, exists := keys[contribution.scopeOwner]; exists {
|
|
contribution.scopeOwner = key
|
|
} else {
|
|
contribution.scopeOwner = reanchorImportKey(contribution.scopeOwner, anchors)
|
|
}
|
|
reanchorAcquisitionScope(contribution.pendingItem, keys, anchors)
|
|
if contribution.nested != nil {
|
|
for nestedIndex := range contribution.nested.acquisition {
|
|
reanchorAcquisitionScope(&contribution.nested.acquisition[nestedIndex], keys, anchors)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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,
|
|
}
|
|
}
|