Files
opentf/internal/lang/exprs/marks.go
Martin Atkins a277aafff9 configgraph: Some unit tests for some "Value" methods
A big part of the motivation of the design of configgraph vs. package tofu
was to keep each node type self-contained and avoid the need for a big
shared "god object" that everything interacts with, and in turn part of
the reason to be interested in that is because it should makes it actually
practical and useful to write unit tests for these node types.

This commit introduces a few unit tests mainly just as a proof-of-concept
to see if the design has lived up to the ambition. So far that seems to be
true, although I'll reserve judgement until there's at least some coverage
on every on of the node types in this package.

Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
2025-10-27 10:15:41 -07:00

122 lines
5.1 KiB
Go

// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package exprs
import (
"github.com/opentofu/opentofu/internal/tfdiags"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/ctymarks"
)
// evaluationMark is the type used for a few cty marks we use to help
// distinguish normal unknown value results from those used as placeholders
// for failed evaluation of upstream objects.
type evalResultMark rune
// EvalError is a [cty.Value] mark used on placeholder unknown values returned
// whenever evaluation causes error diagnostics.
//
// This is intended only for use between collaborators in a subsystem where
// everyone is consistently following this convention as a means to avoid
// redundantly reporting downstream consequences of an upstream problem.
// Use [WithoutEvalErrorMarks] at the boundary of such a subsystem so that
// code in other parts of the system does not need to deal with these marks.
//
// In many cases it's okay to ignore this mark and just use the unknown value
// placeholder as normal, letting the mark "infect" the result as necessary,
// but this is here for less common situations where logic _does_ need to handle
// those situations differently.
//
// For example, if a particular language feature treats the mere presence of
// an unknown value as an error then the error-handling logic should first
// check whether the value has this mark and only return the
// unknown-value-related error if not, because the presence of this mark
// suggests that the unknown value is likely caused by another upstream error
// rather than by the module author directly using an unknown value in an
// invalid location.
//
// The expression evaluation mechanisms in this package add this mark
// automatically whenever they generate evaluation error placeholders, but
// it's exposed as an exported symbol so that logic elsewhere that is performing
// non-expression-based evaluation can participate in this marking scheme.
const EvalError = evalResultMark('E')
func (ee evalResultMark) GoString() string {
return "exprs.EvalError"
}
// AsEvalError returns the given value with [EvalError] applied to it as a mark.
//
// The expression evaluator in this package automatically adds this mark when
// expression evaluation fails, but code elsewhere in the system should use this
// to also treat other kinds of errors as evaluation errors if they are
// returning a placeholder value alongside at least one error diagnostic.
func AsEvalError(v cty.Value) cty.Value {
return v.Mark(EvalError)
}
// EvalResult is a helper that checks whether the given diags contains errors
// and if so returns the given value with [EvalError] applied to it as a
// mark.
//
// Otherwise it returns the value unmodified. In all cases it returns exactly
// the diagnostics it was given.
//
// This is designed for concise use in a return statement in a function that's
// returning both a value and some diagnostics produced from somewhere else,
// to ensure that the [EvalError] mark still gets applied when appropriate.
func EvalResult(v cty.Value, diags tfdiags.Diagnostics) (cty.Value, tfdiags.Diagnostics) {
if diags.HasErrors() {
return AsEvalError(v), diags
}
return v, diags
}
// IsEvalError returns true if the given value is directly marked with
// [EvalError], indicating that it's acting as a placeholder for an upstream
// failed evaluation.
//
// This only checks the given value shallowly. Use [HasEvalErrors] instead to
// check whether there are any evaluation error placeholders in nested values.
// For example, a caller that is using [`cty.Value.IsWhollyKnown`] to reject
// a value with unknown values anywhere inside it should prefer to use
// [HasEvalErrors] first to determine if any of the nested unknown values
// might actually be error placeholders.
func IsEvalError(v cty.Value) bool {
return v.HasMark(EvalError)
}
// HasEvalErrors is like [IsEvalError] except that it visits nested values
// inside the given value recursively and returns true if [EvalError] marks
// are present at any nesting level.
//
// Don't use this except when rejecting values that contain nested unknown
// values in a context where those values are not allowed. If only _shallow_
// unknown values are disallowed then use [IsEvalError] instead to match
// that with only a shallow check for the [EvalError] mark.
func HasEvalErrors(v cty.Value) bool {
_, marks := v.UnmarkDeep()
_, marked := marks[EvalError]
return marked
}
// WithoutEvalErrorMarks returns the given value with any shallow or nested
// [EvalError] marks removed.
//
// Use this at the boundary of a subsystem that uses the evaluation error
// marking scheme internally as an implementation detail, to avoid exposing
// this extra complexity to callers that are merely consuming the finalized
// results.
func WithoutEvalErrorMarks(v cty.Value) cty.Value {
v, _ = v.WrangleMarksDeep(func(mark any, path cty.Path) (ctymarks.WrangleAction, error) {
if mark == EvalError {
return ctymarks.WrangleDrop, nil
}
return nil, nil // Leave all other marks alone.
})
return v
}