terraform-provider-gitea/vendor/github.com/zclconf/go-cty/cty/unknown_refinement.go

748 lines
23 KiB
Go
Raw Normal View History

package cty
import (
"fmt"
"math"
"strings"
"github.com/zclconf/go-cty/cty/ctystrings"
)
// Refine creates a [RefinementBuilder] with which to annotate the reciever
// with zero or more additional refinements that constrain the range of
// the value.
//
// Calling methods on a RefinementBuilder for a known value essentially just
// serves as assertions about the range of that value, leading to panics if
// those assertions don't hold in practice. This is mainly supported just to
// make programs that rely on refinements automatically self-check by using
// the refinement codepath unconditionally on both placeholders and final
// values for those placeholders. It's always a bug to refine the range of
// an unknown value and then later substitute an exact value outside of the
// refined range.
//
// Calling methods on a RefinementBuilder for an unknown value is perhaps
// more useful because the newly-refined value will then be a placeholder for
// a smaller range of values and so it may be possible for other operations
// on the unknown value to return a known result despite the exact value not
// yet being known.
//
// It is never valid to refine [DynamicVal], because that value is a
// placeholder for a value about which we knkow absolutely nothing. A value
// must at least have a known root type before it can support further
// refinement.
func (v Value) Refine() *RefinementBuilder {
v, marks := v.Unmark()
if unk, isUnk := v.v.(*unknownType); isUnk && unk.refinement != nil {
// We're refining a value that's already been refined before, so
// we'll start from a copy of its existing refinements.
wip := unk.refinement.copy()
return &RefinementBuilder{v, marks, wip}
}
ty := v.Type()
var wip unknownValRefinement
switch {
case ty == DynamicPseudoType && !v.IsKnown():
panic("cannot refine an unknown value of an unknown type")
case ty == String:
wip = &refinementString{}
case ty == Number:
wip = &refinementNumber{}
case ty.IsCollectionType():
wip = &refinementCollection{
// A collection can never have a negative length, so we'll
// start with that already constrained.
minLen: 0,
maxLen: math.MaxInt,
}
case ty == Bool || ty.IsObjectType() || ty.IsTupleType() || ty.IsCapsuleType():
// For other known types we'll just track nullability
wip = &refinementNullable{}
case ty == DynamicPseudoType && v.IsNull():
// It's okay in principle to refine a null value of unknown type,
// although all we can refine about it is that it's definitely null and
// so this is pretty pointless and only supported to avoid callers
// always needing to treat this situation as a special case to avoid
// panic.
wip = &refinementNullable{
isNull: tristateTrue,
}
default:
// we leave "wip" as nil for all other types, representing that
// they don't support refinements at all and so any call on the
// RefinementBuilder should fail.
// NOTE: We intentionally don't allow any refinements for
// cty.DynamicVal here, even though it could be nice in principle
// to at least track non-nullness for those, because it's historically
// been valid to directly compare values with cty.DynamicVal using
// the Go "==" operator and recording a refinement for an untyped
// unknown value would break existing code relying on that.
}
return &RefinementBuilder{v, marks, wip}
}
// RefineWith is a variant of Refine which uses callback functions instead of
// the builder pattern.
//
// The result is equivalent to passing the return value of [Value.Refine] to the
// first callback, and then continue passing the builder through any other
// callbacks in turn, and then calling [RefinementBuilder.NewValue] on the
// final result.
//
// The builder pattern approach of [Value.Refine] is more convenient for inline
// annotation of refinements when constructing a value, but this alternative
// approach may be more convenient when applying pre-defined collections of
// refinements, or when refinements are defined separately from the values
// they will apply to.
//
// Each refiner callback should return the same pointer that it was given,
// typically after having mutated it using the [RefinementBuilder] methods.
// It's invalid to return a different builder.
func (v Value) RefineWith(refiners ...func(*RefinementBuilder) *RefinementBuilder) Value {
if len(refiners) == 0 {
return v
}
origBuilder := v.Refine()
builder := origBuilder
for _, refiner := range refiners {
builder = refiner(builder)
if builder != origBuilder {
panic("refiner callback returned a different builder")
}
}
return builder.NewValue()
}
// RefineNotNull is a shorthand for Value.Refine().NotNull().NewValue(), because
// declaring that a unknown value isn't null is by far the most common use of
// refinements.
func (v Value) RefineNotNull() Value {
return v.Refine().NotNull().NewValue()
}
// RefinementBuilder is a supporting type for the [Value.Refine] method,
// using the builder pattern to apply zero or more constraints before
// constructing a new value with all of those constraints applied.
//
// Most of the methods of this type return the same reciever to allow
// for method call chaining. End call chains with a call to
// [RefinementBuilder.NewValue] to obtain the newly-refined value.
type RefinementBuilder struct {
orig Value
marks ValueMarks
wip unknownValRefinement
}
func (b *RefinementBuilder) assertRefineable() {
if b.wip == nil {
panic(fmt.Sprintf("cannot refine a %#v value", b.orig.Type()))
}
}
// NotNull constrains the value as definitely not being null.
//
// NotNull is valid when refining values of the following types:
// - number, boolean, and string values
// - list, set, or map types of any element type
// - values of object types
// - values of collection types
// - values of capsule types
//
// When refining any other type this function will panic.
//
// In particular note that it is not valid to constrain an untyped value
// -- a value whose type is `cty.DynamicPseudoType` -- as being non-null.
// An unknown value of an unknown type is always completely unconstrained.
func (b *RefinementBuilder) NotNull() *RefinementBuilder {
b.assertRefineable()
if b.orig.IsKnown() && b.orig.IsNull() {
panic("refining null value as non-null")
}
if b.wip.null() == tristateTrue {
panic("refining null value as non-null")
}
b.wip.setNull(tristateFalse)
return b
}
// Null constrains the value as definitely null.
//
// Null is valid for the same types as [RefinementBuilder.NotNull].
// When refining any other type this function will panic.
//
// Explicitly cnstraining a value to be null is strange because that suggests
// that the caller does actually know the value -- there is only one null
// value for each type constraint -- but this is here for symmetry with the
// fact that a [ValueRange] can also represent that a value is definitely null.
func (b *RefinementBuilder) Null() *RefinementBuilder {
b.assertRefineable()
if b.orig.IsKnown() && !b.orig.IsNull() {
panic("refining non-null value as null")
}
if b.wip.null() == tristateFalse {
panic("refining non-null value as null")
}
b.wip.setNull(tristateTrue)
return b
}
// NumericRange constrains the upper and/or lower bounds of a number value,
// or panics if this builder is not refining a number value.
//
// The two given values are interpreted as inclusive bounds and either one
// may be an unknown number if only one of the two bounds is currently known.
// If either of the given values is not a non-null number value then this
// function will panic.
func (b *RefinementBuilder) NumberRangeInclusive(min, max Value) *RefinementBuilder {
return b.NumberRangeLowerBound(min, true).NumberRangeUpperBound(max, true)
}
// NumberRangeLowerBound constraints the lower bound of a number value, or
// panics if this builder is not refining a number value.
func (b *RefinementBuilder) NumberRangeLowerBound(min Value, inclusive bool) *RefinementBuilder {
b.assertRefineable()
wip, ok := b.wip.(*refinementNumber)
if !ok {
panic(fmt.Sprintf("cannot refine numeric bounds for a %#v value", b.orig.Type()))
}
if !min.IsKnown() {
// Nothing to do if the lower bound is unknown.
return b
}
if min.IsNull() {
panic("number range lower bound must not be null")
}
if inclusive {
if gt := min.GreaterThan(b.orig); gt.IsKnown() && gt.True() {
panic(fmt.Sprintf("refining %#v to be >= %#v", b.orig, min))
}
} else {
if gt := min.GreaterThanOrEqualTo(b.orig); gt.IsKnown() && gt.True() {
panic(fmt.Sprintf("refining %#v to be > %#v", b.orig, min))
}
}
if wip.min != NilVal {
var ok Value
if inclusive && !wip.minInc {
ok = min.GreaterThan(wip.min)
} else {
ok = min.GreaterThanOrEqualTo(wip.min)
}
if ok.IsKnown() && ok.False() {
return b // Our existing refinement is more constrained
}
}
if min != NegativeInfinity {
wip.min = min
wip.minInc = inclusive
}
wip.assertConsistentBounds()
return b
}
// NumberRangeUpperBound constraints the upper bound of a number value, or
// panics if this builder is not refining a number value.
func (b *RefinementBuilder) NumberRangeUpperBound(max Value, inclusive bool) *RefinementBuilder {
b.assertRefineable()
wip, ok := b.wip.(*refinementNumber)
if !ok {
panic(fmt.Sprintf("cannot refine numeric bounds for a %#v value", b.orig.Type()))
}
if !max.IsKnown() {
// Nothing to do if the upper bound is unknown.
return b
}
if max.IsNull() {
panic("number range upper bound must not be null")
}
if inclusive {
if lt := max.LessThan(b.orig); lt.IsKnown() && lt.True() {
panic(fmt.Sprintf("refining %#v to be <= %#v", b.orig, max))
}
} else {
if lt := max.LessThanOrEqualTo(b.orig); lt.IsKnown() && lt.True() {
panic(fmt.Sprintf("refining %#v to be < %#v", b.orig, max))
}
}
if wip.max != NilVal {
var ok Value
if inclusive && !wip.maxInc {
ok = max.LessThan(wip.max)
} else {
ok = max.LessThanOrEqualTo(wip.max)
}
if ok.IsKnown() && ok.False() {
return b // Our existing refinement is more constrained
}
}
if max != PositiveInfinity {
wip.max = max
wip.maxInc = inclusive
}
wip.assertConsistentBounds()
return b
}
// CollectionLengthLowerBound constrains the lower bound of the length of a
// collection value, or panics if this builder is not refining a collection
// value.
func (b *RefinementBuilder) CollectionLengthLowerBound(min int) *RefinementBuilder {
b.assertRefineable()
wip, ok := b.wip.(*refinementCollection)
if !ok {
panic(fmt.Sprintf("cannot refine collection length bounds for a %#v value", b.orig.Type()))
}
minVal := NumberIntVal(int64(min))
if b.orig.IsKnown() {
realLen := b.orig.Length()
if gt := minVal.GreaterThan(realLen); gt.IsKnown() && gt.True() {
panic(fmt.Sprintf("refining collection of length %#v with lower bound %#v", realLen, min))
}
}
if wip.minLen > min {
return b // Our existing refinement is more constrained
}
wip.minLen = min
wip.assertConsistentLengthBounds()
return b
}
// CollectionLengthUpperBound constrains the upper bound of the length of a
// collection value, or panics if this builder is not refining a collection
// value.
//
// The upper bound must be a known, non-null number or this function will
// panic.
func (b *RefinementBuilder) CollectionLengthUpperBound(max int) *RefinementBuilder {
b.assertRefineable()
wip, ok := b.wip.(*refinementCollection)
if !ok {
panic(fmt.Sprintf("cannot refine collection length bounds for a %#v value", b.orig.Type()))
}
if b.orig.IsKnown() {
maxVal := NumberIntVal(int64(max))
realLen := b.orig.Length()
if lt := maxVal.LessThan(realLen); lt.IsKnown() && lt.True() {
panic(fmt.Sprintf("refining collection of length %#v with upper bound %#v", realLen, max))
}
}
if wip.maxLen < max {
return b // Our existing refinement is more constrained
}
wip.maxLen = max
wip.assertConsistentLengthBounds()
return b
}
// CollectionLength is a shorthand for passing the same length to both
// [CollectionLengthLowerBound] and [CollectionLengthUpperBound].
//
// A collection with a refined length with equal bounds can sometimes collapse
// to a known value. Refining to length zero always produces a known value.
// The behavior for other lengths varies by collection type kind.
//
// If the unknown value is of a set type, it's only valid to use this method
// if the caller knows that there will be the given number of _unique_ values
// in the set. If any values might potentially coalesce together once known,
// use [CollectionLengthUpperBound] instead.
func (b *RefinementBuilder) CollectionLength(length int) *RefinementBuilder {
return b.CollectionLengthLowerBound(length).CollectionLengthUpperBound(length)
}
// StringPrefix constrains the prefix of a string value, or panics if this
// builder is not refining a string value.
//
// The given prefix will be Unicode normalized in the same way that a
// cty.StringVal would be.
//
// Due to Unicode normalization and grapheme cluster rules, appending new
// characters to a string can change the meaning of earlier characters.
// StringPrefix may discard one or more characters from the end of the given
// prefix to avoid that problem.
//
// Although cty cannot check this automatically, applications should avoid
// relying on the discarding of the suffix for correctness. For example, if the
// prefix ends with an emoji base character then StringPrefix will discard it
// in case subsequent characters include emoji modifiers, but it's still
// incorrect for the final string to use an entirely different base character.
//
// Applications which fully control the final result and can guarantee the
// subsequent characters will not combine with the prefix may be able to use
// [RefinementBuilder.StringPrefixFull] instead, after carefully reviewing
// the constraints described in its documentation.
func (b *RefinementBuilder) StringPrefix(prefix string) *RefinementBuilder {
return b.StringPrefixFull(ctystrings.SafeKnownPrefix(prefix))
}
// StringPrefixFull is a variant of StringPrefix that will never shorten the
// given prefix to take into account the possibility of the next character
// combining with the end of the prefix.
//
// Applications which fully control the subsequent characters can use this
// as long as they guarantee that the characters added later cannot possibly
// combine with characters at the end of the prefix to form a single grapheme
// cluster. For example, it would be unsafe to use the full prefix "hello" if
// there is any chance that the final string will add a combining diacritic
// character after the "o", because that would then change the final character.
//
// Use [RefinementBuilder.StringPrefix] instead if an application cannot fully
// control the final result to avoid violating this rule.
func (b *RefinementBuilder) StringPrefixFull(prefix string) *RefinementBuilder {
b.assertRefineable()
wip, ok := b.wip.(*refinementString)
if !ok {
panic(fmt.Sprintf("cannot refine string prefix for a %#v value", b.orig.Type()))
}
// We must apply the same Unicode processing we'd normally use for a
// cty string so that the prefix will be comparable.
prefix = NormalizeString(prefix)
// If we have a known string value then the given prefix must actually
// match it.
if b.orig.IsKnown() && !b.orig.IsNull() {
have := b.orig.AsString()
matchLen := len(have)
if l := len(prefix); l < matchLen {
matchLen = l
}
have = have[:matchLen]
new := prefix[:matchLen]
if have != new {
panic("refined prefix is inconsistent with known value")
}
}
// If we already have a refined prefix then the overlapping parts of that
// and the new prefix must match.
{
matchLen := len(wip.prefix)
if l := len(prefix); l < matchLen {
matchLen = l
}
have := wip.prefix[:matchLen]
new := prefix[:matchLen]
if have != new {
panic("refined prefix is inconsistent with previous refined prefix")
}
}
// We'll only save the new prefix if it's longer than the one we already
// had.
if len(prefix) > len(wip.prefix) {
wip.prefix = prefix
}
return b
}
// NewValue completes the refinement process by constructing a new value
// that is guaranteed to meet all of the previously-specified refinements.
//
// If the original value being refined was known then the result is exactly
// that value, because otherwise the previous refinement calls would have
// panicked reporting the refinements as invalid for the value.
//
// If the original value was unknown then the result is typically also unknown
// but may have additional refinements compared to the original. If the applied
// refinements have reduced the range to a single exact value then the result
// might be that known value.
func (b *RefinementBuilder) NewValue() (ret Value) {
defer func() {
// Regardless of how we return, the new value should have the same
// marks as our original value.
ret = ret.WithMarks(b.marks)
}()
if b.orig.IsKnown() {
return b.orig
}
// We have a few cases where the value has been refined enough that we now
// know exactly what the value is, or at least we can produce a more
// detailed approximation of it.
switch b.wip.null() {
case tristateTrue:
// There is only one null value of each type so this is now known.
return NullVal(b.orig.Type())
case tristateFalse:
// If we know it's definitely not null then we might have enough
// information to construct a known, non-null value.
if rfn, ok := b.wip.(*refinementNumber); ok {
// If both bounds are inclusive and equal then our value can
// only be the same number as the bounds.
if rfn.maxInc && rfn.minInc {
if rfn.min != NilVal && rfn.max != NilVal {
eq := rfn.min.Equals(rfn.max)
if eq.IsKnown() && eq.True() {
return rfn.min
}
}
}
} else if rfn, ok := b.wip.(*refinementCollection); ok {
// If both of the bounds are equal then we know the length is
// the same number as the bounds.
if rfn.minLen == rfn.maxLen {
knownLen := rfn.minLen
ty := b.orig.Type()
if knownLen == 0 {
// If we know the length is zero then we can construct
// a known value of any collection kind.
switch {
case ty.IsListType():
return ListValEmpty(ty.ElementType())
case ty.IsSetType():
return SetValEmpty(ty.ElementType())
case ty.IsMapType():
return MapValEmpty(ty.ElementType())
}
} else if ty.IsListType() {
// If we know the length of the list then we can
// create a known list with unknown elements instead
// of a wholly-unknown list.
elems := make([]Value, knownLen)
unk := UnknownVal(ty.ElementType())
for i := range elems {
elems[i] = unk
}
return ListVal(elems)
} else if ty.IsSetType() && knownLen == 1 {
// If we know we have a one-element set then we
// know the one element can't possibly coalesce with
// anything else and so we can create a known set with
// an unknown element.
return SetVal([]Value{UnknownVal(ty.ElementType())})
}
}
}
}
return Value{
ty: b.orig.ty,
v: &unknownType{refinement: b.wip},
}
}
// unknownValRefinment is an interface pretending to be a sum type representing
// the different kinds of unknown value refinements we support for different
// types of value.
type unknownValRefinement interface {
unknownValRefinementSigil()
copy() unknownValRefinement
null() tristateBool
setNull(tristateBool)
rawEqual(other unknownValRefinement) bool
GoString() string
}
type refinementString struct {
refinementNullable
prefix string
}
func (r *refinementString) unknownValRefinementSigil() {}
func (r *refinementString) copy() unknownValRefinement {
ret := *r
// Everything in refinementString is immutable, so a shallow copy is sufficient.
return &ret
}
func (r *refinementString) rawEqual(other unknownValRefinement) bool {
{
other, ok := other.(*refinementString)
if !ok {
return false
}
return (r.refinementNullable.rawEqual(&other.refinementNullable) &&
r.prefix == other.prefix)
}
}
func (r *refinementString) GoString() string {
var b strings.Builder
b.WriteString(r.refinementNullable.GoString())
if r.prefix != "" {
fmt.Fprintf(&b, ".StringPrefixFull(%q)", r.prefix)
}
return b.String()
}
type refinementNumber struct {
refinementNullable
min, max Value
minInc, maxInc bool
}
func (r *refinementNumber) unknownValRefinementSigil() {}
func (r *refinementNumber) copy() unknownValRefinement {
ret := *r
// Everything in refinementNumber is immutable, so a shallow copy is sufficient.
return &ret
}
func (r *refinementNumber) rawEqual(other unknownValRefinement) bool {
{
other, ok := other.(*refinementNumber)
if !ok {
return false
}
return (r.refinementNullable.rawEqual(&other.refinementNullable) &&
r.min.RawEquals(other.min) &&
r.max.RawEquals(other.max) &&
r.minInc == other.minInc &&
r.maxInc == other.maxInc)
}
}
func (r *refinementNumber) GoString() string {
var b strings.Builder
b.WriteString(r.refinementNullable.GoString())
if r.min != NilVal && r.min != NegativeInfinity {
fmt.Fprintf(&b, ".NumberLowerBound(%#v, %t)", r.min, r.minInc)
}
if r.max != NilVal && r.max != PositiveInfinity {
fmt.Fprintf(&b, ".NumberUpperBound(%#v, %t)", r.max, r.maxInc)
}
return b.String()
}
func (r *refinementNumber) assertConsistentBounds() {
if r.min == NilVal || r.max == NilVal {
return // If only one bound is constrained then there's nothing to be inconsistent with
}
var ok Value
if r.minInc != r.maxInc {
ok = r.min.LessThan(r.max)
} else {
ok = r.min.LessThanOrEqualTo(r.max)
}
if ok.IsKnown() && ok.False() {
panic(fmt.Sprintf("number lower bound %#v is greater than upper bound %#v", r.min, r.max))
}
}
type refinementCollection struct {
refinementNullable
minLen, maxLen int
}
func (r *refinementCollection) unknownValRefinementSigil() {}
func (r *refinementCollection) copy() unknownValRefinement {
ret := *r
// Everything in refinementCollection is immutable, so a shallow copy is sufficient.
return &ret
}
func (r *refinementCollection) rawEqual(other unknownValRefinement) bool {
{
other, ok := other.(*refinementCollection)
if !ok {
return false
}
return (r.refinementNullable.rawEqual(&other.refinementNullable) &&
r.minLen == other.minLen &&
r.maxLen == other.maxLen)
}
}
func (r *refinementCollection) GoString() string {
var b strings.Builder
b.WriteString(r.refinementNullable.GoString())
if r.minLen != 0 {
fmt.Fprintf(&b, ".CollectionLengthLowerBound(%d)", r.minLen)
}
if r.maxLen != math.MaxInt {
fmt.Fprintf(&b, ".CollectionLengthUpperBound(%d)", r.maxLen)
}
return b.String()
}
func (r *refinementCollection) assertConsistentLengthBounds() {
if r.maxLen < r.minLen {
panic(fmt.Sprintf("collection length upper bound %d is less than lower bound %d", r.maxLen, r.minLen))
}
}
type refinementNullable struct {
isNull tristateBool
}
func (r *refinementNullable) unknownValRefinementSigil() {}
func (r *refinementNullable) copy() unknownValRefinement {
ret := *r
// Everything in refinementJustNull is immutable, so a shallow copy is sufficient.
return &ret
}
func (r *refinementNullable) null() tristateBool {
return r.isNull
}
func (r *refinementNullable) setNull(v tristateBool) {
r.isNull = v
}
func (r *refinementNullable) rawEqual(other unknownValRefinement) bool {
{
other, ok := other.(*refinementNullable)
if !ok {
return false
}
return r.isNull == other.isNull
}
}
func (r *refinementNullable) GoString() string {
switch r.isNull {
case tristateFalse:
return ".NotNull()"
case tristateTrue:
return ".Null()"
default:
return ""
}
}
type tristateBool rune
const tristateTrue tristateBool = 'T'
const tristateFalse tristateBool = 'F'
const tristateUnknown tristateBool = 0