ExtractLayerGraph builds a directed graph of Layer composition from the given AST nodes. It performs a DFS with a two-pass approach using an explicit work stack: - First pass: push children for processing - Second pass: link processed children into the graph
( tp *typeparser.TypeParser, c *checker.Checker, nodes []*ast.Node, sf *ast.SourceFile, opts ExtractLayerGraphOptions, )
| 21 | // - First pass: push children for processing |
| 22 | // - Second pass: link processed children into the graph |
| 23 | func ExtractLayerGraph( |
| 24 | tp *typeparser.TypeParser, |
| 25 | c *checker.Checker, |
| 26 | nodes []*ast.Node, |
| 27 | sf *ast.SourceFile, |
| 28 | opts ExtractLayerGraphOptions, |
| 29 | ) *graph.Graph[LayerGraphNodeInfo, LayerGraphEdgeInfo] { |
| 30 | g := graph.New[LayerGraphNodeInfo, LayerGraphEdgeInfo]() |
| 31 | nodeToGraphIndex := make(map[*ast.Node]graph.NodeIndex) |
| 32 | visitedNodes := make(map[*ast.Node]bool) |
| 33 | nodeInPipeContext := make(map[*ast.Node]bool) |
| 34 | depthBudget := make(map[*ast.Node]int) |
| 35 | |
| 36 | // Resolve the Layer module import name for ExplodeOnlyLayerCalls checks. |
| 37 | layerModuleName := "" |
| 38 | if !opts.SkipExplode { |
| 39 | layerModuleName = findLayerModuleName(sf) |
| 40 | } |
| 41 | |
| 42 | // Initialize the work stack with the root nodes. |
| 43 | stack := []workItem{} |
| 44 | |
| 45 | appendNodeToVisit := func(n *ast.Node, depth int) { |
| 46 | depthBudget[n] = depth |
| 47 | stack = append(stack, workItem{node: n, depth: depth}) |
| 48 | } |
| 49 | for _, node := range nodes { |
| 50 | appendNodeToVisit(node, opts.FollowSymbolsDepth) |
| 51 | } |
| 52 | |
| 53 | addNode := func(n *ast.Node, info LayerGraphNodeInfo) graph.NodeIndex { |
| 54 | idx := g.AddNode(info) |
| 55 | nodeToGraphIndex[n] = idx |
| 56 | return idx |
| 57 | } |
| 58 | |
| 59 | for len(stack) > 0 { |
| 60 | // Pop from the stack (LIFO). |
| 61 | item := stack[len(stack)-1] |
| 62 | stack = stack[:len(stack)-1] |
| 63 | current := item.node |
| 64 | currentDepth := depthBudget[current] |
| 65 | |
| 66 | // Case 1: Pipe detection |
| 67 | if !opts.SkipExplode { |
| 68 | if pipeResult := tp.ParsePipeCall(current); pipeResult != nil { |
| 69 | if !visitedNodes[current] { |
| 70 | // First pass: push self back, then subject and args. |
| 71 | appendNodeToVisit(current, currentDepth) |
| 72 | appendNodeToVisit(pipeResult.Subject, currentDepth) |
| 73 | for _, arg := range pipeResult.Args { |
| 74 | appendNodeToVisit(arg, currentDepth) |
| 75 | nodeInPipeContext[arg] = true |
| 76 | } |
| 77 | visitedNodes[current] = true |
| 78 | } else { |
| 79 | // Second pass: collect child graph indices. |
| 80 | allChildren := make([]*ast.Node, 0, 1+len(pipeResult.Args)) |
no test coverage detected