Preserve caller scope during requirement inspection

This commit is contained in:
2026-10-08 21:15:56 +00:00
parent 21815ad7c0
commit e69a847cf2
4 changed files with 150 additions and 5 deletions

View File

@@ -22,6 +22,7 @@ type dotIdentity uint8
const (
dotRoot dotIdentity = iota
dotNonRoot
dotUnknown
)
type inputEvidence struct {
@@ -175,6 +176,14 @@ func (walker *requirementWalker) evaluateCommand(command *parse.CommandNode, rea
func (walker *requirementWalker) evaluateValue(node parse.Node, reach requirementReachability, dot dotIdentity) conditionEvaluation {
switch node := node.(type) {
case *parse.DotNode:
if dot == dotRoot {
return conditionEvaluation{value: walker.vars, known: true}
}
if dot == dotUnknown {
walker.unresolvedVariableScope(reach)
}
return conditionEvaluation{unsupported: true}
case *parse.StringNode:
return conditionEvaluation{value: node.Text, known: true}
case *parse.BoolNode:
@@ -183,6 +192,9 @@ func (walker *requirementWalker) evaluateValue(node parse.Node, reach requiremen
return conditionEvaluation{value: nil, known: true}
case *parse.FieldNode:
if dot != dotRoot || len(node.Ident) == 0 {
if dot == dotUnknown {
walker.unresolvedVariableScope(reach)
}
return conditionEvaluation{unsupported: true}
}
name := node.Ident[0]
@@ -197,7 +209,11 @@ func (walker *requirementWalker) evaluateValue(node parse.Node, reach requiremen
}
return conditionEvaluation{value: value, known: true}
case *parse.VariableNode:
if len(node.Ident) == 1 && node.Ident[0] == "$" && walker.rootKnown {
return conditionEvaluation{value: walker.vars, known: true}
}
if len(node.Ident) < 2 || node.Ident[0] != "$" || !walker.rootKnown {
walker.visitVariable(node, reach)
return conditionEvaluation{unsupported: true}
}
name := node.Ident[1]
@@ -483,7 +499,8 @@ func (walker *requirementWalker) visit(node parse.Node, reach requirementReachab
if err := walker.visit(node.Pipe, requirementInactive, dot); err != nil {
return err
}
if err := walker.visit(node.List, requirementInactive, dotNonRoot); err != nil {
bodyDot := walker.withDotIdentity(node.Pipe, dot, conditionEvaluation{})
if err := walker.visit(node.List, requirementInactive, bodyDot); err != nil {
return err
}
return walker.visit(node.ElseList, requirementInactive, dot)
@@ -492,7 +509,8 @@ func (walker *requirementWalker) visit(node parse.Node, reach requirementReachab
before := walker.dependencies
thenReach := branchReach(reach, condition.known, condition.truth(), true)
elseReach := branchReach(reach, condition.known, condition.truth(), false)
if err := walker.visit(node.List, thenReach, dotNonRoot); err != nil {
bodyDot := walker.withDotIdentity(node.Pipe, dot, condition)
if err := walker.visit(node.List, thenReach, bodyDot); err != nil {
return err
}
if err := walker.visit(node.ElseList, elseReach, dot); err != nil {
@@ -561,6 +579,13 @@ func (walker *requirementWalker) visit(node parse.Node, reach requirementReachab
walker.addIssue("", node.Ident[0], "A chained field access is outside the direct-key scope.")
}
}
if dot == dotUnknown {
walker.unresolvedVariableScope(reach)
}
case *parse.DotNode:
if dot == dotUnknown {
walker.unresolvedVariableScope(reach)
}
case *parse.VariableNode:
walker.visitVariable(node, reach)
case *parse.ChainNode:
@@ -668,6 +693,11 @@ func (walker *requirementWalker) visitCommand(command *parse.CommandNode, reach
walker.recordOptionalVariable(key, reach)
return nil
}
// A local map may alias the caller root even though its value is not
// tracked. A lookup on it must not vanish from complete discovery.
if variable, ok := unwrapSingleValue(command.Args[1]).(*parse.VariableNode); ok && len(variable.Ident) == 1 {
walker.unresolvedVariableScope(reach)
}
}
if !isIdentifier && len(command.Args) == 1 {
return walker.visit(command.Args[0], reach, dot)
@@ -692,7 +722,11 @@ func (walker *requirementWalker) visitCommand(command *parse.CommandNode, reach
}
func (walker *requirementWalker) visitVariable(variable *parse.VariableNode, reach requirementReachability) {
if len(variable.Ident) < 2 || variable.Ident[0] != "$" {
if len(variable.Ident) < 2 {
return
}
if variable.Ident[0] != "$" {
walker.unresolvedVariableScope(reach)
return
}
if !walker.rootKnown {
@@ -708,8 +742,35 @@ func (walker *requirementWalker) visitVariable(variable *parse.VariableNode, rea
}
}
func (walker *requirementWalker) unresolvedVariableScope(reach requirementReachability) {
if reach != requirementInactive {
walker.unknownVariableEffect = true
walker.addIssue("", "", "A caller-variable scope is not statically known.")
}
}
func (walker *requirementWalker) withDotIdentity(pipe *parse.PipeNode, dot dotIdentity, condition conditionEvaluation) dotIdentity {
if pipe != nil && len(pipe.Cmds) == 1 && len(pipe.Decl) == 0 && !pipe.IsAssign && len(pipe.Cmds[0].Args) == 1 &&
isRootMap(pipe.Cmds[0].Args[0], dot, walker.rootKnown) {
return dotRoot
}
if condition.known {
if _, root := condition.value.(map[string]string); root {
return dotRoot
}
return dotNonRoot
}
if condition.inputNameUnknown ||
!condition.unsupported && (len(condition.missingVariables) > 0 || len(condition.contentInputs) > 0) {
// Supported caller values and input bodies are strings; supported
// comparisons and negation return booleans, never the root map.
return dotNonRoot
}
return dotUnknown
}
func isRootMap(node parse.Node, dot dotIdentity, rootKnown bool) bool {
switch node := node.(type) {
switch node := unwrapSingleValue(node).(type) {
case *parse.DotNode:
return dot == dotRoot
case *parse.VariableNode:
@@ -719,6 +780,16 @@ func isRootMap(node parse.Node, dot dotIdentity, rootKnown bool) bool {
}
}
func unwrapSingleValue(node parse.Node) parse.Node {
for {
pipe, ok := node.(*parse.PipeNode)
if !ok || len(pipe.Cmds) != 1 || len(pipe.Decl) != 0 || pipe.IsAssign || len(pipe.Cmds[0].Args) != 1 {
return node
}
node = pipe.Cmds[0].Args[0]
}
}
func (walker *requirementWalker) recordInput(name string, reach requirementReachability) {
evidence := walker.inputs[name]
if evidence == nil {