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 scheduleAcquisitionScope(scope acquisitionScope, graph importInclusionGraph) acquisitionScope { positions := make(map[importNodeKey]int, len(scope.contributions)) slots := make([]int, 0, len(scope.contributions)) keys := make([]dependencyKey, 0, len(scope.contributions)) for index, contribution := range scope.contributions { if contribution.scopeOwner != scope.owner { continue } positions[contribution.identity] = len(keys) slots = append(slots, index) keys = append(keys, contribution.identity.dependencyKey()) } if len(keys) < 2 { return scope } edges := make([]dependencyEdge, 0, len(graph.edges)) for _, edge := range graph.edges { importer, importerOK := positions[edge.importer] imported, importedOK := positions[edge.imported] if importerOK && importedOK { edges = append(edges, dependencyEdge{prerequisite: imported, dependent: importer}) } } order, _ := scheduleDependencies(keys, edges) if len(order) != len(keys) { return scope } scheduled := scope scheduled.contributions = append([]acquiredContribution(nil), scope.contributions...) for position, scheduledIndex := range order { scheduled.contributions[slots[position]] = scope.contributions[slots[scheduledIndex]] } return scheduled } 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, } }