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:
Steve Yegge
2025-12-25 02:33:22 -08:00
parent 84c124fc3d
commit 127a36dd5f
2 changed files with 32 additions and 9 deletions

View File

@@ -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 {

View File

@@ -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) {