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