Formula cycle detection: show full extends chain in error message (gt-8tmz.15)
When bd cook encounters a circular extends chain (A extends B extends A), the error message now shows the full chain: "cycle-a -> cycle-b -> cycle-a" instead of just "circular extends detected: cycle-a". This makes debugging circular dependencies much easier by showing exactly which formulas are involved in the cycle. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -24,8 +24,11 @@ type Parser struct {
|
||||
// cache stores loaded formulas by name.
|
||||
cache map[string]*Formula
|
||||
|
||||
// resolving tracks formulas currently being resolved (for cycle detection).
|
||||
resolving map[string]bool
|
||||
// resolvingSet tracks formulas currently being resolved (for cycle detection).
|
||||
resolvingSet map[string]bool
|
||||
|
||||
// resolvingChain tracks the order of formulas being resolved (for error messages).
|
||||
resolvingChain []string
|
||||
}
|
||||
|
||||
// NewParser creates a new formula parser.
|
||||
@@ -37,9 +40,10 @@ func NewParser(searchPaths ...string) *Parser {
|
||||
paths = defaultSearchPaths()
|
||||
}
|
||||
return &Parser{
|
||||
searchPaths: paths,
|
||||
cache: make(map[string]*Formula),
|
||||
resolving: make(map[string]bool),
|
||||
searchPaths: paths,
|
||||
cache: make(map[string]*Formula),
|
||||
resolvingSet: make(map[string]bool),
|
||||
resolvingChain: nil,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,11 +122,17 @@ func (p *Parser) Parse(data []byte) (*Formula, error) {
|
||||
// Returns a new formula with all inheritance applied.
|
||||
func (p *Parser) Resolve(formula *Formula) (*Formula, error) {
|
||||
// Check for cycles
|
||||
if p.resolving[formula.Formula] {
|
||||
return nil, fmt.Errorf("circular extends detected: %s", formula.Formula)
|
||||
if p.resolvingSet[formula.Formula] {
|
||||
// Build the cycle chain for a clear error message
|
||||
chain := append(p.resolvingChain, formula.Formula)
|
||||
return nil, fmt.Errorf("circular extends detected: %s", strings.Join(chain, " -> "))
|
||||
}
|
||||
p.resolving[formula.Formula] = true
|
||||
defer delete(p.resolving, formula.Formula)
|
||||
p.resolvingSet[formula.Formula] = true
|
||||
p.resolvingChain = append(p.resolvingChain, formula.Formula)
|
||||
defer func() {
|
||||
delete(p.resolvingSet, formula.Formula)
|
||||
p.resolvingChain = p.resolvingChain[:len(p.resolvingChain)-1]
|
||||
}()
|
||||
|
||||
// If no extends, just validate and return
|
||||
if len(formula.Extends) == 0 {
|
||||
|
||||
@@ -3,6 +3,7 @@ package formula
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -533,6 +534,18 @@ func TestResolve_CircularExtends(t *testing.T) {
|
||||
if err == nil {
|
||||
t.Error("Resolve should fail for circular extends")
|
||||
}
|
||||
|
||||
// Verify the error message shows the full cycle chain
|
||||
errStr := err.Error()
|
||||
if !strings.Contains(errStr, "cycle-a") {
|
||||
t.Errorf("error should mention cycle-a: %v", err)
|
||||
}
|
||||
if !strings.Contains(errStr, "cycle-b") {
|
||||
t.Errorf("error should mention cycle-b: %v", err)
|
||||
}
|
||||
if !strings.Contains(errStr, "->") {
|
||||
t.Errorf("error should show cycle chain with '->': %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetStepByID(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user