Memoize flag alias resolution by context
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# Consolidated audit catalog
|
||||
|
||||
Status: **12 distinct open findings: 12 P3**, consolidated from 30
|
||||
Status: **11 distinct open findings: 11 P3**, consolidated from 30
|
||||
numbered entries in seven audit documents. Reviewed audit state:
|
||||
`db732e3a1de55c844f77870e1c6db683c2685d5d`.
|
||||
|
||||
@@ -274,6 +274,10 @@ ownership; it is separate from the closed-reporting-handle retention in F07.
|
||||
|
||||
## F13 — P3: Flag-alias compilation repeatedly walks shared chain suffixes
|
||||
|
||||
**Complete.** Each effective context memoizes its target resolutions, including
|
||||
inactive endpoints, while retaining canonical precedence and cycle diagnostics.
|
||||
Focused alias and discovery tests pass, including shared suffixes and registration.
|
||||
|
||||
For A chained effective flag mappings across C contexts, independent target
|
||||
walks can perform Θ(CA²) target examinations, including inactive chains. Each
|
||||
source repeats work already performed for another source's suffix.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Audit remediation implementation plan
|
||||
|
||||
Status: in progress; Stages 1–14 complete.
|
||||
Status: in progress; Stages 1–15 complete.
|
||||
|
||||
Baseline: `d492c7747175c01a53261206c2b4356e53453621`. Before this sprint,
|
||||
intervening changes recorded decisions and retired the completed older
|
||||
@@ -500,6 +500,12 @@ clear, with selected syntax and provenance preserved.
|
||||
|
||||
## Stage 15 — Memoize each context's flag-alias graph
|
||||
|
||||
**Complete.** Context-local traversal memoizes canonical and unavailable
|
||||
endpoints, with an active stack for cycle diagnostics. Source review confirms
|
||||
resolved suffixes are reused within each context. Focused alias, registration,
|
||||
cycle and discovery tests passed offline with Go 1.27.0 and `-mod=readonly`,
|
||||
including shared suffixes and unavailable destinations.
|
||||
|
||||
Findings: F13. Source: `flag_aliases.go`.
|
||||
|
||||
1. Replace independent suffix walks with context-local visitation and memoized
|
||||
|
||||
@@ -230,14 +230,54 @@ func resolveFlagAliases(context flagAliasContext, mappings map[string]flagAliasM
|
||||
sources = append(sources, source)
|
||||
}
|
||||
sort.Strings(sources)
|
||||
type terminal struct {
|
||||
spelling capturedFlagSpelling
|
||||
active bool
|
||||
}
|
||||
resolved := make(map[string]terminal)
|
||||
visiting := make(map[string]int)
|
||||
var stack []capturedFlagSpelling
|
||||
var resolve func(capturedFlagSpelling) (terminal, error)
|
||||
resolve = func(current capturedFlagSpelling) (terminal, error) {
|
||||
key := flagSpellingKey(current)
|
||||
if endpoint, exists := context.canonical[key]; exists {
|
||||
return terminal{spelling: endpoint, active: true}, nil
|
||||
}
|
||||
if result, exists := resolved[key]; exists {
|
||||
return result, nil
|
||||
}
|
||||
if start, exists := visiting[key]; exists {
|
||||
cycle := append(append([]capturedFlagSpelling(nil), stack[start:]...), current)
|
||||
spellings := make([]string, 0, len(cycle))
|
||||
for _, spelling := range cycle {
|
||||
spellings = append(spellings, spelling.token())
|
||||
}
|
||||
return terminal{}, fmt.Errorf("flag alias cycle: %s", strings.Join(spellings, " → "))
|
||||
}
|
||||
mapping, exists := mappings[key]
|
||||
if !exists {
|
||||
resolved[key] = terminal{}
|
||||
return terminal{}, nil
|
||||
}
|
||||
visiting[key] = len(stack)
|
||||
stack = append(stack, current)
|
||||
result, err := resolve(mapping.target)
|
||||
stack = stack[:len(stack)-1]
|
||||
delete(visiting, key)
|
||||
if err != nil {
|
||||
return terminal{}, err
|
||||
}
|
||||
resolved[key] = result
|
||||
return result, nil
|
||||
}
|
||||
for _, sourceKey := range sources {
|
||||
mapping := mappings[sourceKey]
|
||||
source := spellingFromKey(sourceKey)
|
||||
target, active, err := resolveFlagTarget(source, mapping.target, mappings, context.canonical)
|
||||
terminal, err := resolve(mapping.target)
|
||||
if err != nil {
|
||||
return resolvedFlagAliases{}, fmt.Errorf("%s: %w", flagContextDescription(context), err)
|
||||
}
|
||||
if !active {
|
||||
if !terminal.active {
|
||||
if mapping.scoped {
|
||||
return resolvedFlagAliases{}, fmt.Errorf("scoped flag alias %s has no canonical destination in %s", source.token(), flagContextDescription(context))
|
||||
}
|
||||
@@ -246,38 +286,11 @@ func resolveFlagAliases(context flagAliasContext, mappings map[string]flagAliasM
|
||||
if _, collision := context.canonical[sourceKey]; collision {
|
||||
return resolvedFlagAliases{}, fmt.Errorf("flag alias %s conflicts with a canonical spelling in %s", source.token(), flagContextDescription(context))
|
||||
}
|
||||
result.bySpelling[sourceKey] = resolvedFlagAlias{source: source, target: target}
|
||||
result.bySpelling[sourceKey] = resolvedFlagAlias{source: source, target: terminal.spelling}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func resolveFlagTarget(source, target capturedFlagSpelling, mappings map[string]flagAliasMapping, canonical map[string]capturedFlagSpelling) (capturedFlagSpelling, bool, error) {
|
||||
visited := map[string]int{flagSpellingKey(source): 0}
|
||||
chain := []capturedFlagSpelling{source}
|
||||
current := target
|
||||
for {
|
||||
key := flagSpellingKey(current)
|
||||
if endpoint, exists := canonical[key]; exists {
|
||||
return endpoint, true, nil
|
||||
}
|
||||
if start, exists := visited[key]; exists {
|
||||
cycle := append(append([]capturedFlagSpelling(nil), chain[start:]...), current)
|
||||
spellings := make([]string, 0, len(cycle))
|
||||
for _, spelling := range cycle {
|
||||
spellings = append(spellings, spelling.token())
|
||||
}
|
||||
return capturedFlagSpelling{}, false, fmt.Errorf("flag alias cycle: %s", strings.Join(spellings, " → "))
|
||||
}
|
||||
visited[key] = len(chain)
|
||||
chain = append(chain, current)
|
||||
next, exists := mappings[key]
|
||||
if !exists {
|
||||
return capturedFlagSpelling{}, false, nil
|
||||
}
|
||||
current = next.target
|
||||
}
|
||||
}
|
||||
|
||||
func spellingFromKey(key string) capturedFlagSpelling {
|
||||
if strings.HasPrefix(key, "short:") {
|
||||
return capturedFlagSpelling{name: strings.TrimPrefix(key, "short:"), short: true}
|
||||
|
||||
@@ -295,3 +295,21 @@ func capturedSpelling(token string) capturedFlagSpelling {
|
||||
issues := []error(nil)
|
||||
return captureFlagSpelling("test", token, &issues)
|
||||
}
|
||||
|
||||
func TestFlagAliasSharedSuffixAndUnavailableDestination(t *testing.T) {
|
||||
app, err := New(Identity{Name: "Example"}, flagAliasCommand("active", stringFlag("color", "")), flagAliasCommand("inactive"),
|
||||
FlagAlias("--first", "--common"), FlagAlias("--second", "--common"), FlagAlias("--common", "--color"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, name := range []string{"first", "second", "common"} {
|
||||
key := flagSpellingKey(capturedFlagSpelling{name: name})
|
||||
active := app.flagAliases[commandpath.Path{"active"}.Key()].bySpelling
|
||||
if alias, ok := active[key]; !ok || alias.target.token() != "--color" {
|
||||
t.Fatalf("active %s = %#v", name, alias)
|
||||
}
|
||||
if _, ok := app.flagAliases[commandpath.Path{"inactive"}.Key()].bySpelling[key]; ok {
|
||||
t.Fatalf("inactive %s was installed", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user