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 stores loaded formulas by name.
|
||||||
cache map[string]*Formula
|
cache map[string]*Formula
|
||||||
|
|
||||||
// resolving tracks formulas currently being resolved (for cycle detection).
|
// resolvingSet tracks formulas currently being resolved (for cycle detection).
|
||||||
resolving map[string]bool
|
resolvingSet map[string]bool
|
||||||
|
|
||||||
|
// resolvingChain tracks the order of formulas being resolved (for error messages).
|
||||||
|
resolvingChain []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewParser creates a new formula parser.
|
// NewParser creates a new formula parser.
|
||||||
@@ -37,9 +40,10 @@ func NewParser(searchPaths ...string) *Parser {
|
|||||||
paths = defaultSearchPaths()
|
paths = defaultSearchPaths()
|
||||||
}
|
}
|
||||||
return &Parser{
|
return &Parser{
|
||||||
searchPaths: paths,
|
searchPaths: paths,
|
||||||
cache: make(map[string]*Formula),
|
cache: make(map[string]*Formula),
|
||||||
resolving: make(map[string]bool),
|
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.
|
// Returns a new formula with all inheritance applied.
|
||||||
func (p *Parser) Resolve(formula *Formula) (*Formula, error) {
|
func (p *Parser) Resolve(formula *Formula) (*Formula, error) {
|
||||||
// Check for cycles
|
// Check for cycles
|
||||||
if p.resolving[formula.Formula] {
|
if p.resolvingSet[formula.Formula] {
|
||||||
return nil, fmt.Errorf("circular extends detected: %s", 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
|
p.resolvingSet[formula.Formula] = true
|
||||||
defer delete(p.resolving, formula.Formula)
|
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 no extends, just validate and return
|
||||||
if len(formula.Extends) == 0 {
|
if len(formula.Extends) == 0 {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package formula
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -533,6 +534,18 @@ func TestResolve_CircularExtends(t *testing.T) {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
t.Error("Resolve should fail for circular extends")
|
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) {
|
func TestGetStepByID(t *testing.T) {
|
||||||
|
|||||||
Reference in New Issue
Block a user