terraformDummyRepo2/vendor/github.com/zclconf/go-cty/cty/value_range.go

409 lines
13 KiB
Go
Raw Normal View History

package cty
import (
"fmt"
"math"
"strings"
)
// Range returns an object that offers partial information about the range
// of the receiver.
//
// This is most relevant for unknown values, because it gives access to any
// optional additional constraints on the final value (specified by the source
// of the value using "refinements") beyond what we can assume from the value's
// type.
//
// Calling Range for a known value is a little strange, but it's supported by
// returning a [ValueRange] object that describes the exact value as closely
// as possible. Typically a caller should work directly with the exact value
// in that case, but some purposes might only need the level of detail
// offered by ranges and so can share code between both known and unknown
// values.
func (v Value) Range() ValueRange {
// For an unknown value we just use its own refinements.
if unk, isUnk := v.v.(*unknownType); isUnk {
refinement := unk.refinement
if refinement == nil {
// We'll generate an unconstrained refinement, just to
// simplify the code in ValueRange methods which can
// therefore assume that there's always a refinement.
refinement = &refinementNullable{isNull: tristateUnknown}
}
return ValueRange{v.Type(), refinement}
}
if v.IsNull() {
// If we know a value is null then we'll just report that,
// since no other refinements make sense for a definitely-null value.
return ValueRange{
v.Type(),
&refinementNullable{isNull: tristateTrue},
}
}
// For a known value we construct synthetic refinements that match
// the value, just as a convenience for callers that want to share
// codepaths between both known and unknown values.
ty := v.Type()
var synth unknownValRefinement
switch {
case ty == String:
synth = &refinementString{
prefix: v.AsString(),
}
case ty == Number:
synth = &refinementNumber{
min: v,
max: v,
minInc: true,
maxInc: true,
}
case ty.IsCollectionType():
if lenVal := v.Length(); lenVal.IsKnown() {
l, _ := lenVal.AsBigFloat().Int64()
synth = &refinementCollection{
minLen: int(l),
maxLen: int(l),
}
} else {
synth = &refinementCollection{
minLen: 0,
maxLen: math.MaxInt,
}
}
default:
// If we don't have anything else to say then we can at least
// guarantee that the value isn't null.
synth = &refinementNullable{}
}
// If we get down here then the value is definitely not null
synth.setNull(tristateFalse)
return ValueRange{ty, synth}
}
// ValueRange offers partial information about the range of a value.
//
// This is primarily interesting for unknown values, because it provides access
// to any additional known constraints (specified using "refinements") on the
// range of the value beyond what is represented by the value's type.
type ValueRange struct {
ty Type
raw unknownValRefinement
}
// TypeConstraint returns a type constraint describing the value's type as
// precisely as possible with the available information.
func (r ValueRange) TypeConstraint() Type {
return r.ty
}
// CouldBeNull returns true unless the value being described is definitely
// known to represent a non-null value.
func (r ValueRange) CouldBeNull() bool {
if r.raw == nil {
// A totally-unconstrained unknown value could be null
return true
}
return r.raw.null() != tristateFalse
}
// DefinitelyNotNull returns true if there are no null values in the range.
func (r ValueRange) DefinitelyNotNull() bool {
if r.raw == nil {
// A totally-unconstrained unknown value could be null
return false
}
return r.raw.null() == tristateFalse
}
// NumberLowerBound returns information about the lower bound of the range of
// a number value, or panics if the value is definitely not a number.
//
// If the value is nullable then the result represents the range of the number
// only if it turns out not to be null.
//
// The resulting value might itself be an unknown number if there is no
// known lower bound. In that case the "inclusive" flag is meaningless.
func (r ValueRange) NumberLowerBound() (min Value, inclusive bool) {
if r.ty == DynamicPseudoType {
// We don't even know if this is a number yet.
return UnknownVal(Number), false
}
if r.ty != Number {
panic(fmt.Sprintf("NumberLowerBound for %#v", r.ty))
}
if rfn, ok := r.raw.(*refinementNumber); ok && rfn.min != NilVal {
if !rfn.min.IsKnown() {
return NegativeInfinity, true
}
return rfn.min, rfn.minInc
}
return NegativeInfinity, false
}
// NumberUpperBound returns information about the upper bound of the range of
// a number value, or panics if the value is definitely not a number.
//
// If the value is nullable then the result represents the range of the number
// only if it turns out not to be null.
//
// The resulting value might itself be an unknown number if there is no
// known upper bound. In that case the "inclusive" flag is meaningless.
func (r ValueRange) NumberUpperBound() (max Value, inclusive bool) {
if r.ty == DynamicPseudoType {
// We don't even know if this is a number yet.
return UnknownVal(Number), false
}
if r.ty != Number {
panic(fmt.Sprintf("NumberUpperBound for %#v", r.ty))
}
if rfn, ok := r.raw.(*refinementNumber); ok && rfn.max != NilVal {
if !rfn.max.IsKnown() {
return PositiveInfinity, true
}
return rfn.max, rfn.maxInc
}
return PositiveInfinity, false
}
// StringPrefix returns a string that is guaranteed to be the prefix of
// the string value being described, or panics if the value is definitely not
// a string.
//
// If the value is nullable then the result represents the prefix of the string
// only if it turns out to not be null.
//
// If the resulting value is zero-length then the value could potentially be
// a string but it has no known prefix.
//
// cty.String values always contain normalized UTF-8 sequences; the result is
// also guaranteed to be a normalized UTF-8 sequence so the result also
// represents the exact bytes of the string value's prefix.
func (r ValueRange) StringPrefix() string {
if r.ty == DynamicPseudoType {
// We don't even know if this is a string yet.
return ""
}
if r.ty != String {
panic(fmt.Sprintf("StringPrefix for %#v", r.ty))
}
if rfn, ok := r.raw.(*refinementString); ok {
return rfn.prefix
}
return ""
}
// LengthLowerBound returns information about the lower bound of the length of
// a collection-typed value, or panics if the value is definitely not a
// collection.
//
// If the value is nullable then the result represents the range of the length
// only if the value turns out not to be null.
func (r ValueRange) LengthLowerBound() int {
if r.ty == DynamicPseudoType {
// We don't even know if this is a collection yet.
return 0
}
if !r.ty.IsCollectionType() {
panic(fmt.Sprintf("LengthLowerBound for %#v", r.ty))
}
if rfn, ok := r.raw.(*refinementCollection); ok {
return rfn.minLen
}
return 0
}
// LengthUpperBound returns information about the upper bound of the length of
// a collection-typed value, or panics if the value is definitely not a
// collection.
//
// If the value is nullable then the result represents the range of the length
// only if the value turns out not to be null.
//
// The resulting value might itself be an unknown number if there is no
// known upper bound. In that case the "inclusive" flag is meaningless.
func (r ValueRange) LengthUpperBound() int {
if r.ty == DynamicPseudoType {
// We don't even know if this is a collection yet.
return math.MaxInt
}
if !r.ty.IsCollectionType() {
panic(fmt.Sprintf("LengthUpperBound for %#v", r.ty))
}
if rfn, ok := r.raw.(*refinementCollection); ok {
return rfn.maxLen
}
return math.MaxInt
}
// Includes determines whether the given value is in the receiving range.
//
// It can return only three possible values:
// - [cty.True] if the range definitely includes the value
// - [cty.False] if the range definitely does not include the value
// - An unknown value of [cty.Bool] if there isn't enough information to decide.
//
// This function is not fully comprehensive: it may return an unknown value
// in some cases where a definitive value could be computed in principle, and
// those same situations may begin returning known values in later releases as
// the rules are refined to be more complete. Currently the rules focus mainly
// on answering [cty.False], because disproving membership tends to be more
// useful than proving membership.
func (r ValueRange) Includes(v Value) Value {
unknownResult := UnknownVal(Bool).RefineNotNull()
if r.raw.null() == tristateTrue {
if v.IsNull() {
return True
} else {
return False
}
}
if r.raw.null() == tristateFalse {
if v.IsNull() {
return False
}
// A definitely-not-null value could potentially match
// but we won't know until we do some more checks below.
}
// If our range includes both null and non-null values and the value is
// null then it's definitely in range.
if v.IsNull() {
return True
}
if len(v.Type().TestConformance(r.TypeConstraint())) != 0 {
// If the value doesn't conform to the type constraint then it's
// definitely not in the range.
return False
}
if v.Type() == DynamicPseudoType {
// If it's an unknown value of an unknown type then there's no
// further tests we can make.
return unknownResult
}
switch r.raw.(type) {
case *refinementString:
if v.IsKnown() {
prefix := r.StringPrefix()
got := v.AsString()
if !strings.HasPrefix(got, prefix) {
return False
}
}
case *refinementCollection:
lenVal := v.Length()
minLen := NumberIntVal(int64(r.LengthLowerBound()))
maxLen := NumberIntVal(int64(r.LengthUpperBound()))
if minOk := lenVal.GreaterThanOrEqualTo(minLen); minOk.IsKnown() && minOk.False() {
return False
}
if maxOk := lenVal.LessThanOrEqualTo(maxLen); maxOk.IsKnown() && maxOk.False() {
return False
}
case *refinementNumber:
minVal, minInc := r.NumberLowerBound()
maxVal, maxInc := r.NumberUpperBound()
var minOk, maxOk Value
if minInc {
minOk = v.GreaterThanOrEqualTo(minVal)
} else {
minOk = v.GreaterThan(minVal)
}
if maxInc {
maxOk = v.LessThanOrEqualTo(maxVal)
} else {
maxOk = v.LessThan(maxVal)
}
if minOk.IsKnown() && minOk.False() {
return False
}
if maxOk.IsKnown() && maxOk.False() {
return False
}
}
// If we fall out here then we don't have enough information to decide.
return unknownResult
}
// numericRangeArithmetic is a helper we use to calculate derived numeric ranges
// for arithmetic on refined numeric values.
//
// op must be a monotone operation. numericRangeArithmetic adapts that operation
// into the equivalent interval arithmetic operation.
//
// The result is a superset of the range of the given operation against the
// given input ranges, if it's possible to calculate that without encountering
// an invalid operation. Currently the result is inexact due to ignoring
// the inclusiveness of the input bounds and just always returning inclusive
// bounds.
func numericRangeArithmetic(op func(a, b Value) Value, a, b ValueRange) func(*RefinementBuilder) *RefinementBuilder {
wrapOp := func(a, b Value) (ret Value) {
// Our functions have various panicking edge cases involving incompatible
// uses of infinities. To keep things simple here we'll catch those
// and just return an unconstrained number.
defer func() {
if v := recover(); v != nil {
ret = UnknownVal(Number)
}
}()
return op(a, b)
}
return func(builder *RefinementBuilder) *RefinementBuilder {
aMin, _ := a.NumberLowerBound()
aMax, _ := a.NumberUpperBound()
bMin, _ := b.NumberLowerBound()
bMax, _ := b.NumberUpperBound()
v1 := wrapOp(aMin, bMin)
v2 := wrapOp(aMin, bMax)
v3 := wrapOp(aMax, bMin)
v4 := wrapOp(aMax, bMax)
newMin := mostNumberValue(Value.LessThan, v1, v2, v3, v4)
newMax := mostNumberValue(Value.GreaterThan, v1, v2, v3, v4)
if isInf := newMin.Equals(NegativeInfinity); isInf.IsKnown() && isInf.False() {
builder = builder.NumberRangeLowerBound(newMin, true)
}
if isInf := newMax.Equals(PositiveInfinity); isInf.IsKnown() && isInf.False() {
builder = builder.NumberRangeUpperBound(newMax, true)
}
return builder
}
}
func mostNumberValue(op func(i, j Value) Value, v1 Value, vN ...Value) Value {
r := v1
for _, v := range vN {
more := op(v, r)
if !more.IsKnown() {
return UnknownVal(Number)
}
if more.True() {
r = v
}
}
return r
}
// definitelyNotNull is a convenient helper for the common situation of checking
// whether a value could possibly be null.
//
// Returns true if the given value is either a known value that isn't null
// or an unknown value that has been refined to exclude null values from its
// range.
func definitelyNotNull(v Value) bool {
if v.IsKnown() {
return !v.IsNull()
}
return v.Range().DefinitelyNotNull()
}