Bump github.com/hashicorp/terraform-plugin-sdk/v2 from 2.24.1 to 2.26.0

Bumps [github.com/hashicorp/terraform-plugin-sdk/v2](https://github.com/hashicorp/terraform-plugin-sdk) from 2.24.1 to 2.26.0.
- [Release notes](https://github.com/hashicorp/terraform-plugin-sdk/releases)
- [Changelog](https://github.com/hashicorp/terraform-plugin-sdk/blob/main/CHANGELOG.md)
- [Commits](https://github.com/hashicorp/terraform-plugin-sdk/compare/v2.24.1...v2.26.0)

---
updated-dependencies:
- dependency-name: github.com/hashicorp/terraform-plugin-sdk/v2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
This commit is contained in:
dependabot[bot]
2023-03-20 20:25:53 +00:00
committed by GitHub
parent 4f33464489
commit 84c9110a24
502 changed files with 39722 additions and 9679 deletions

View File

@ -43,7 +43,7 @@ func getConversion(in cty.Type, out cty.Type, unsafe bool) conversion {
out = out.WithoutOptionalAttributesDeep()
if !isKnown {
return cty.UnknownVal(dynamicReplace(in.Type(), out)), nil
return prepareUnknownResult(in.Range(), dynamicReplace(in.Type(), out)), nil
}
if isNull {
@ -199,3 +199,64 @@ func retConversion(conv conversion) Conversion {
return conv(in, cty.Path(nil))
}
}
// prepareUnknownResult can apply value refinements to a returned unknown value
// in certain cases where characteristics of the source value or type can
// transfer into range constraints on the result value.
func prepareUnknownResult(sourceRange cty.ValueRange, targetTy cty.Type) cty.Value {
sourceTy := sourceRange.TypeConstraint()
ret := cty.UnknownVal(targetTy)
if sourceRange.DefinitelyNotNull() {
ret = ret.RefineNotNull()
}
switch {
case sourceTy.IsObjectType() && targetTy.IsMapType():
// A map built from an object type always has the same number of
// elements as the source type has attributes.
return ret.Refine().CollectionLength(len(sourceTy.AttributeTypes())).NewValue()
case sourceTy.IsTupleType() && targetTy.IsListType():
// A list built from a typle type always has the same number of
// elements as the source type has elements.
return ret.Refine().CollectionLength(sourceTy.Length()).NewValue()
case sourceTy.IsTupleType() && targetTy.IsSetType():
// When building a set from a tuple type we can't exactly constrain
// the length because some elements might coalesce, but we can
// guarantee an upper limit. We can also guarantee at least one
// element if the tuple isn't empty.
switch l := sourceTy.Length(); l {
case 0, 1:
return ret.Refine().CollectionLength(l).NewValue()
default:
return ret.Refine().
CollectionLengthLowerBound(1).
CollectionLengthUpperBound(sourceTy.Length()).
NewValue()
}
case sourceTy.IsCollectionType() && targetTy.IsCollectionType():
// NOTE: We only reach this function if there is an available
// conversion between the source and target type, so we don't
// need to repeat element type compatibility checks and such here.
//
// If the source value already has a refined length then we'll
// transfer those refinements to the result, because conversion
// does not change length (aside from set element coalescing).
b := ret.Refine()
if targetTy.IsSetType() {
if sourceRange.LengthLowerBound() > 0 {
// If the source has at least one element then the result
// must always have at least one too, because value coalescing
// cannot totally empty the set.
b = b.CollectionLengthLowerBound(1)
}
} else {
b = b.CollectionLengthLowerBound(sourceRange.LengthLowerBound())
}
b = b.CollectionLengthUpperBound(sourceRange.LengthUpperBound())
return b.NewValue()
default:
return ret
}
}

26
vendor/github.com/zclconf/go-cty/cty/ctystrings/doc.go generated vendored Normal file
View File

@ -0,0 +1,26 @@
// Package ctystrings is a collection of string manipulation utilities which
// intend to help application developers implement string-manipulation
// functionality in a way that respects the cty model of strings, even when
// they are working in the realm of Go strings.
//
// cty strings are, internally, NFC-normalized as defined in Unicode Standard
// Annex #15 and encoded as UTF-8.
//
// When working with [cty.Value] of string type cty manages this
// automatically as an implementation detail, but when applications call
// [Value.AsString] they will receive a value that has been subjected to that
// normalization, and so may need to take that normalization into account when
// manipulating the resulting string or comparing it with other Go strings
// that did not originate in a [cty.Value].
//
// Although the core representation of [cty.String] only considers whole
// strings, it's also conventional in other locations such as the standard
// library functions to consider strings as being sequences of grapheme
// clusters as defined by Unicode Standard Annex #29, which adds further
// rules about combining multiple consecutive codepoints together into a
// single user-percieved character. Functions that work with substrings should
// always use grapheme clusters as their smallest unit of splitting strings,
// and never break strings in the middle of a grapheme cluster. The functions
// in this package respect that convention unless otherwise stated in their
// documentation.
package ctystrings

View File

@ -0,0 +1,14 @@
package ctystrings
import (
"golang.org/x/text/unicode/norm"
)
// Normalize applies NFC normalization to the given string, returning the
// transformed string.
//
// This function achieves the same effect as wrapping a string in a value
// using [cty.StringVal] and then unwrapping it again using [Value.AsString].
func Normalize(str string) string {
return norm.NFC.String(str)
}

View File

@ -0,0 +1,139 @@
package ctystrings
import (
"fmt"
"unicode/utf8"
"github.com/apparentlymart/go-textseg/v13/textseg"
"golang.org/x/text/unicode/norm"
)
// SafeKnownPrefix takes a string intended to represent a known prefix of
// another string and modifies it so that it would be safe to use with
// byte-based prefix matching against another NFC-normalized string. It
// also takes into account grapheme cluster boundaries and trims off any
// suffix that could potentially be an incomplete grapheme cluster.
//
// Specifically, SafeKnownPrefix first applies NFC normalization to the prefix
// and then trims off one or more characters from the end of the string which
// could potentially be transformed into a different character if another
// string were appended to it. For example, a trailing latin letter will
// typically be trimmed because appending a combining diacritic mark would
// transform it into a different character.
//
// This transformation is important whenever the remainder of the string is
// arbitrary user input not directly controlled by the application. If an
// application can guarantee that the remainder of the string will not begin
// with combining marks then it is safe to instead just normalize the prefix
// string with [Normalize].
//
// Note that this function only takes into account normalization boundaries
// and does _not_ take into account grapheme cluster boundaries as defined
// by Unicode Standard Annex #29.
func SafeKnownPrefix(prefix string) string {
prefix = Normalize(prefix)
// Our starting approach here is essentially what a streaming parser would
// do when consuming a Unicode string in chunks and needing to determine
// what prefix of the current buffer is safe to process without waiting for
// more information, which is described in TR15 section 13.1
// "Buffering with Unicode Normalization":
// https://unicode.org/reports/tr15/#Buffering_with_Unicode_Normalization
//
// The general idea here is to find the last character in the string that
// could potentially start a sequence of codepoints that would combine
// together, and then truncate the string to exclude that character and
// everything after it.
form := norm.NFC
lastBoundary := form.LastBoundary([]byte(prefix))
if lastBoundary != -1 && lastBoundary != len(prefix) {
prefix = prefix[:lastBoundary]
// If we get here then we've already shortened the prefix and so
// further analysis below is unnecessary because it would be relying
// on an incomplete prefix anyway.
return prefix
}
// Now we'll use the textseg package's grapheme cluster scanner to scan
// as far through the string as we can without the scanner telling us
// that it would need more bytes to decide.
//
// This step is conservative because the grapheme cluster rules are not
// designed with prefix-matching in mind. In the base case we'll just
// always discard the last grapheme cluster, although we do have some
// special cases for trailing codepoints that can't possibly combine with
// subsequent codepoints to form a single grapheme cluster and which seem
// likely to arise often in practical use.
remain := []byte(prefix)
prevBoundary := 0
thisBoundary := 0
for len(remain) > 0 {
advance, _, err := textseg.ScanGraphemeClusters(remain, false)
if err != nil {
// ScanGraphemeClusters should never return an error because
// any sequence of valid UTF-8 encodings is valid input.
panic(fmt.Sprintf("textseg.ScanGraphemeClusters returned error: %s", err))
}
if advance == 0 {
// If we have at least one byte remaining but the scanner cannot
// advance then that means the remainder might be an incomplete
// grapheme cluster and so we need to stop here, discarding the
// rest of the input. However, we do now know that we can safely
// include what we found on the previous iteration of this loop.
prevBoundary = thisBoundary
break
}
prevBoundary = thisBoundary
thisBoundary += advance
remain = remain[advance:]
}
// This is our heuristic for detecting cases where we can be sure that
// the above algorithm was too conservative because the last segment
// we found is definitely not subject to the grapheme cluster "do not split"
// rules.
suspect := prefix[prevBoundary:thisBoundary]
if sequenceMustEndGraphemeCluster(suspect) {
prevBoundary = thisBoundary
}
return prefix[:prevBoundary]
}
// sequenceMustEndGraphemeCluster is a heuristic we use to avoid discarding
// the final grapheme cluster of a prefix in SafeKnownPrefix by recognizing
// that a particular sequence is one known to not be subject to any of
// the UAX29 "do not break" rules.
//
// If this function returns true then it is safe to include the given byte
// sequence at the end of a safe prefix. Otherwise we don't know whether or
// not it is safe.
func sequenceMustEndGraphemeCluster(s string) bool {
// For now we're only considering sequences that represent a single
// codepoint. We'll assume that any sequence of two or more codepoints
// that could be a grapheme cluster might be extendable.
if utf8.RuneCountInString(s) != 1 {
return false
}
r, _ := utf8.DecodeRuneInString(s)
// Our initial ruleset is focused on characters that are commonly used
// as delimiters in text intended for both human and machine use, such
// as JSON documents.
//
// We don't include any letters or digits of any script here intentionally
// because those are the ones most likely to be subject to combining rules
// in either current or future Unicode specifications.
//
// We can safely grow this set over time, but we should be very careful
// about shrinking it because it could cause value refinements to loosen
// and thus cause results that were once known to become unknown.
switch r {
case '-', '_', ':', ';', '/', '\\', ',', '.', '(', ')', '{', '}', '[', ']', '|', '?', '!', '~', ' ', '\t', '@', '#', '$', '%', '^', '&', '*', '+', '"', '\'':
return true
default:
return false
}
}

View File

@ -39,6 +39,19 @@ type Spec struct {
// depending on its arguments.
Type TypeFunc
// RefineResult is an optional callback for describing additional
// refinements for the result value beyond what can be described using
// a type constraint.
//
// A refinement callback should always return the same builder it was
// given, typically after modifying it using the methods of
// [cty.RefinementBuilder].
//
// Any refinements described by this callback must hold for the entire
// range of results from the function. For refinements that only apply
// to certain results, use direct refinement within [Impl] instead.
RefineResult func(*cty.RefinementBuilder) *cty.RefinementBuilder
// Impl is the ImplFunc that implements the function's behavior.
//
// Functions are expected to behave as pure functions, and not create
@ -109,20 +122,13 @@ func (f Function) ReturnType(argTypes []cty.Type) (cty.Type, error) {
return f.ReturnTypeForValues(vals)
}
// ReturnTypeForValues is similar to ReturnType but can be used if the caller
// already knows the values of some or all of the arguments, in which case
// the function may be able to determine a more definite result if its
// return type depends on the argument *values*.
//
// For any arguments whose values are not known, pass an Unknown value of
// the appropriate type.
func (f Function) ReturnTypeForValues(args []cty.Value) (ty cty.Type, err error) {
func (f Function) returnTypeForValues(args []cty.Value) (ty cty.Type, dynTypedArgs bool, err error) {
var posArgs []cty.Value
var varArgs []cty.Value
if f.spec.VarParam == nil {
if len(args) != len(f.spec.Params) {
return cty.Type{}, fmt.Errorf(
return cty.Type{}, false, fmt.Errorf(
"wrong number of arguments (%d required; %d given)",
len(f.spec.Params), len(args),
)
@ -132,7 +138,7 @@ func (f Function) ReturnTypeForValues(args []cty.Value) (ty cty.Type, err error)
varArgs = nil
} else {
if len(args) < len(f.spec.Params) {
return cty.Type{}, fmt.Errorf(
return cty.Type{}, false, fmt.Errorf(
"wrong number of arguments (at least %d required; %d given)",
len(f.spec.Params), len(args),
)
@ -161,7 +167,7 @@ func (f Function) ReturnTypeForValues(args []cty.Value) (ty cty.Type, err error)
}
if val.IsNull() && !spec.AllowNull {
return cty.Type{}, NewArgErrorf(i, "argument must not be null")
return cty.Type{}, false, NewArgErrorf(i, "argument must not be null")
}
// AllowUnknown is ignored for type-checking, since we expect to be
@ -171,13 +177,13 @@ func (f Function) ReturnTypeForValues(args []cty.Value) (ty cty.Type, err error)
if val.Type() == cty.DynamicPseudoType {
if !spec.AllowDynamicType {
return cty.DynamicPseudoType, nil
return cty.DynamicPseudoType, true, nil
}
} else if errs := val.Type().TestConformance(spec.Type); errs != nil {
// For now we'll just return the first error in the set, since
// we don't have a good way to return the whole list here.
// Would be good to do something better at some point...
return cty.Type{}, NewArgError(i, errs[0])
return cty.Type{}, false, NewArgError(i, errs[0])
}
}
@ -196,18 +202,18 @@ func (f Function) ReturnTypeForValues(args []cty.Value) (ty cty.Type, err error)
}
if val.IsNull() && !spec.AllowNull {
return cty.Type{}, NewArgErrorf(realI, "argument must not be null")
return cty.Type{}, false, NewArgErrorf(realI, "argument must not be null")
}
if val.Type() == cty.DynamicPseudoType {
if !spec.AllowDynamicType {
return cty.DynamicPseudoType, nil
return cty.DynamicPseudoType, true, nil
}
} else if errs := val.Type().TestConformance(spec.Type); errs != nil {
// For now we'll just return the first error in the set, since
// we don't have a good way to return the whole list here.
// Would be good to do something better at some point...
return cty.Type{}, NewArgError(i, errs[0])
return cty.Type{}, false, NewArgError(i, errs[0])
}
}
}
@ -221,17 +227,53 @@ func (f Function) ReturnTypeForValues(args []cty.Value) (ty cty.Type, err error)
}
}()
return f.spec.Type(args)
ty, err = f.spec.Type(args)
return ty, false, err
}
// ReturnTypeForValues is similar to ReturnType but can be used if the caller
// already knows the values of some or all of the arguments, in which case
// the function may be able to determine a more definite result if its
// return type depends on the argument *values*.
//
// For any arguments whose values are not known, pass an Unknown value of
// the appropriate type.
func (f Function) ReturnTypeForValues(args []cty.Value) (ty cty.Type, err error) {
ty, _, err = f.returnTypeForValues(args)
return ty, err
}
// Call actually calls the function with the given arguments, which must
// conform to the function's parameter specification or an error will be
// returned.
func (f Function) Call(args []cty.Value) (val cty.Value, err error) {
expectedType, err := f.ReturnTypeForValues(args)
expectedType, dynTypeArgs, err := f.returnTypeForValues(args)
if err != nil {
return cty.NilVal, err
}
if dynTypeArgs {
// returnTypeForValues sets this if any argument was inexactly typed
// and the corresponding parameter did not indicate it could deal with
// that. In that case we also avoid calling the implementation function
// because it will also typically not be ready to deal with that case.
return cty.UnknownVal(expectedType), nil
}
if refineResult := f.spec.RefineResult; refineResult != nil {
// If this function has a refinement callback then we'll refine
// our result value in the same way regardless of how we return.
// It's the function author's responsibility to ensure that the
// refinements they specify are valid for the full range of possible
// return values from the function. If not, this will panic when
// detecting an inconsistency.
defer func() {
if val != cty.NilVal {
if val.IsKnown() || val.Type() != cty.DynamicPseudoType {
val = val.RefineWith(refineResult)
}
}
}()
}
// Type checking already dealt with most situations relating to our
// parameter specification, but we still need to deal with unknown

View File

@ -15,7 +15,8 @@ var NotFunc = function.New(&function.Spec{
AllowMarked: true,
},
},
Type: function.StaticReturnType(cty.Bool),
Type: function.StaticReturnType(cty.Bool),
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
return args[0].Not(), nil
},
@ -37,7 +38,8 @@ var AndFunc = function.New(&function.Spec{
AllowMarked: true,
},
},
Type: function.StaticReturnType(cty.Bool),
Type: function.StaticReturnType(cty.Bool),
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
return args[0].And(args[1]), nil
},
@ -59,7 +61,8 @@ var OrFunc = function.New(&function.Spec{
AllowMarked: true,
},
},
Type: function.StaticReturnType(cty.Bool),
Type: function.StaticReturnType(cty.Bool),
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
return args[0].Or(args[1]), nil
},

View File

@ -38,7 +38,8 @@ var BytesLenFunc = function.New(&function.Spec{
AllowDynamicType: true,
},
},
Type: function.StaticReturnType(cty.Number),
Type: function.StaticReturnType(cty.Number),
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
bufPtr := args[0].EncapsulatedValue().(*[]byte)
return cty.NumberIntVal(int64(len(*bufPtr))), nil
@ -65,7 +66,8 @@ var BytesSliceFunc = function.New(&function.Spec{
AllowDynamicType: true,
},
},
Type: function.StaticReturnType(Bytes),
Type: function.StaticReturnType(Bytes),
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
bufPtr := args[0].EncapsulatedValue().(*[]byte)

View File

@ -32,6 +32,7 @@ var HasIndexFunc = function.New(&function.Spec{
}
return cty.Bool, nil
},
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
return args[0].HasIndex(args[1]), nil
},
@ -114,6 +115,7 @@ var LengthFunc = function.New(&function.Spec{
Name: "collection",
Type: cty.DynamicPseudoType,
AllowDynamicType: true,
AllowUnknown: true,
AllowMarked: true,
},
},
@ -124,6 +126,7 @@ var LengthFunc = function.New(&function.Spec{
}
return cty.Number, nil
},
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
return args[0].Length(), nil
},
@ -251,6 +254,7 @@ var CoalesceListFunc = function.New(&function.Spec{
return last, nil
},
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
for _, arg := range args {
if !arg.IsKnown() {
@ -283,7 +287,8 @@ var CompactFunc = function.New(&function.Spec{
Type: cty.List(cty.String),
},
},
Type: function.StaticReturnType(cty.List(cty.String)),
Type: function.StaticReturnType(cty.List(cty.String)),
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
listVal := args[0]
if !listVal.IsWhollyKnown() {
@ -324,7 +329,8 @@ var ContainsFunc = function.New(&function.Spec{
Type: cty.DynamicPseudoType,
},
},
Type: function.StaticReturnType(cty.Bool),
Type: function.StaticReturnType(cty.Bool),
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
arg := args[0]
ty := arg.Type()
@ -382,6 +388,7 @@ var DistinctFunc = function.New(&function.Spec{
Type: func(args []cty.Value) (cty.Type, error) {
return args[0].Type(), nil
},
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
listVal := args[0]
@ -426,6 +433,7 @@ var ChunklistFunc = function.New(&function.Spec{
Type: func(args []cty.Value) (cty.Type, error) {
return cty.List(args[0].Type()), nil
},
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
listVal := args[0]
sizeVal := args[1]
@ -513,6 +521,7 @@ var FlattenFunc = function.New(&function.Spec{
}
return cty.Tuple(tys), nil
},
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
inputList := args[0]
@ -611,6 +620,7 @@ var KeysFunc = function.New(&function.Spec{
return cty.DynamicPseudoType, function.NewArgErrorf(0, "must have map or object type")
}
},
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
// We must unmark the value before we can use ElementIterator on it, and
// then re-apply the same marks (possibly none) when we return. Since we
@ -832,6 +842,7 @@ var MergeFunc = function.New(&function.Spec{
return cty.Object(attrs), nil
},
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
outputMap := make(map[string]cty.Value)
var markses []cty.ValueMarks // remember any marked maps/objects we find
@ -891,6 +902,7 @@ var ReverseListFunc = function.New(&function.Spec{
return cty.NilType, function.NewArgErrorf(0, "can only reverse list or tuple values, not %s", argTy.FriendlyName())
}
},
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
in, marks := args[0].Unmark()
inVals := in.AsValueSlice()
@ -919,10 +931,11 @@ var SetProductFunc = function.New(&function.Spec{
Description: `Calculates the cartesian product of two or more sets.`,
Params: []function.Parameter{},
VarParam: &function.Parameter{
Name: "sets",
Description: "The sets to consider. Also accepts lists and tuples, and if all arguments are of list or tuple type then the result will preserve the input ordering",
Type: cty.DynamicPseudoType,
AllowMarked: true,
Name: "sets",
Description: "The sets to consider. Also accepts lists and tuples, and if all arguments are of list or tuple type then the result will preserve the input ordering",
Type: cty.DynamicPseudoType,
AllowMarked: true,
AllowUnknown: true,
},
Type: func(args []cty.Value) (retType cty.Type, err error) {
if len(args) < 2 {
@ -964,6 +977,7 @@ var SetProductFunc = function.New(&function.Spec{
}
return cty.Set(cty.Tuple(elemTys)), nil
},
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
ety := retType.ElementType()
var retMarks cty.ValueMarks
@ -976,7 +990,7 @@ var SetProductFunc = function.New(&function.Spec{
// Continue processing after we find an argument with unknown
// length to ensure that we cover all the marks
if !arg.Length().IsKnown() {
if !(arg.IsKnown() && arg.Length().IsKnown()) {
hasUnknownLength = true
continue
}
@ -988,7 +1002,62 @@ var SetProductFunc = function.New(&function.Spec{
}
if hasUnknownLength {
return cty.UnknownVal(retType).WithMarks(retMarks), nil
defer func() {
// We're definitely going to return from somewhere in this
// branch and however we do it we must reapply the marks
// on the way out.
ret = ret.WithMarks(retMarks)
}()
ret := cty.UnknownVal(retType)
// Even if we don't know the exact length we may be able to
// constrain the upper and lower bounds of the resulting length.
maxLength := 1
for _, arg := range args {
arg, _ := arg.Unmark() // safe to discard marks because "retMarks" already contains them all
argRng := arg.Range()
ty := argRng.TypeConstraint()
var argMaxLen int
if ty.IsCollectionType() {
argMaxLen = argRng.LengthUpperBound()
} else if ty.IsTupleType() {
argMaxLen = ty.Length()
} else {
// Should not get here but if we do then we'll just
// bail out with an unrefined unknown value.
return ret, nil
}
// The upper bound of a totally-unrefined collection is
// math.MaxInt, which will quickly get us to integer overflow
// here, and so out of pragmatism we'll just impose a reasonable
// upper limit on what is a useful bound to track and return
// unrefined for unusually-large input.
if argMaxLen > 1024 { // arbitrarily-decided threshold
return ret, nil
}
maxLength *= argMaxLen
if maxLength > 2048 { // arbitrarily-decided threshold
return ret, nil
}
if maxLength < 0 { // Seems like we already overflowed, then.
return ret, nil
}
}
if maxLength == 0 {
// This refinement will typically allow the unknown value to
// collapse into a known empty collection.
ret = ret.Refine().CollectionLength(0).NewValue()
} else {
// If we know there's a nonzero maximum number of elements then
// set element coalescing cannot reduce to fewer than one
// element.
ret = ret.Refine().
CollectionLengthLowerBound(1).
CollectionLengthUpperBound(maxLength).
NewValue()
}
return ret, nil
}
if total == 0 {
@ -1101,6 +1170,7 @@ var SliceFunc = function.New(&function.Spec{
}
return cty.Tuple(argTy.TupleElementTypes()[startIndex:endIndex]), nil
},
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
inputList, marks := args[0].Unmark()
@ -1215,6 +1285,7 @@ var ValuesFunc = function.New(&function.Spec{
}
return cty.NilType, errors.New("values() requires a map as the first argument")
},
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
mapVar := args[0]
@ -1303,6 +1374,7 @@ var ZipmapFunc = function.New(&function.Spec{
return cty.NilType, errors.New("values argument must be a list or tuple value")
}
},
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
keys := args[0]
values := args[1]

View File

@ -87,3 +87,36 @@ func MakeToFunc(wantTy cty.Type) function.Function {
},
})
}
// AssertNotNullFunc is a function which does nothing except return an error
// if the argument given to it is null.
//
// This could be useful in some cases where the automatic refinment of
// nullability isn't precise enough, because the result is guaranteed to not
// be null and can therefore allow downstream comparisons to null to return
// a known value even if the value is otherwise unknown.
var AssertNotNullFunc = function.New(&function.Spec{
Description: "Returns the given value varbatim if it is non-null, or raises an error if it's null.",
Params: []function.Parameter{
{
Name: "v",
Type: cty.DynamicPseudoType,
// NOTE: We intentionally don't set AllowNull here, and so
// the function system will automatically reject a null argument
// for us before calling Impl.
},
},
Type: func(args []cty.Value) (cty.Type, error) {
return args[0].Type(), nil
},
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
// Our argument doesn't set AllowNull: true, so we're guaranteed to
// have a non-null value in args[0].
return args[0], nil
},
})
func AssertNotNull(v cty.Value) (cty.Value, error) {
return AssertNotNullFunc.Call([]cty.Value{v})
}

View File

@ -43,6 +43,7 @@ var CSVDecodeFunc = function.New(&function.Spec{
}
return cty.List(cty.Object(atys)), nil
},
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
ety := retType.ElementType()
atys := ety.AttributeTypes()

View File

@ -23,7 +23,8 @@ var FormatDateFunc = function.New(&function.Spec{
Type: cty.String,
},
},
Type: function.StaticReturnType(cty.String),
Type: function.StaticReturnType(cty.String),
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
formatStr := args[0].AsString()
timeStr := args[1].AsString()
@ -281,67 +282,6 @@ func FormatDate(format cty.Value, timestamp cty.Value) (cty.Value, error) {
return FormatDateFunc.Call([]cty.Value{format, timestamp})
}
func parseTimestamp(ts string) (time.Time, error) {
t, err := time.Parse(time.RFC3339, ts)
if err != nil {
switch err := err.(type) {
case *time.ParseError:
// If err is s time.ParseError then its string representation is not
// appropriate since it relies on details of Go's strange date format
// representation, which a caller of our functions is not expected
// to be familiar with.
//
// Therefore we do some light transformation to get a more suitable
// error that should make more sense to our callers. These are
// still not awesome error messages, but at least they refer to
// the timestamp portions by name rather than by Go's example
// values.
if err.LayoutElem == "" && err.ValueElem == "" && err.Message != "" {
// For some reason err.Message is populated with a ": " prefix
// by the time package.
return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp%s", err.Message)
}
var what string
switch err.LayoutElem {
case "2006":
what = "year"
case "01":
what = "month"
case "02":
what = "day of month"
case "15":
what = "hour"
case "04":
what = "minute"
case "05":
what = "second"
case "Z07:00":
what = "UTC offset"
case "T":
return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp: missing required time introducer 'T'")
case ":", "-":
if err.ValueElem == "" {
return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp: end of string where %q is expected", err.LayoutElem)
} else {
return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp: found %q where %q is expected", err.ValueElem, err.LayoutElem)
}
default:
// Should never get here, because time.RFC3339 includes only the
// above portions, but since that might change in future we'll
// be robust here.
what = "timestamp segment"
}
if err.ValueElem == "" {
return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp: end of string before %s", what)
} else {
return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp: cannot use %q as %s", err.ValueElem, what)
}
}
return time.Time{}, err
}
return t, nil
}
// splitDataFormat is a bufio.SplitFunc used to tokenize a date format.
func splitDateFormat(data []byte, atEOF bool) (advance int, token []byte, err error) {
if len(data) == 0 {
@ -418,6 +358,75 @@ func startsDateFormatVerb(b byte) bool {
return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z')
}
func parseTimestamp(ts string) (time.Time, error) {
t, err := parseStrictRFC3339(ts)
if err != nil {
switch err := err.(type) {
case *time.ParseError:
// If err is s time.ParseError then its string representation is not
// appropriate since it relies on details of Go's strange date format
// representation, which a caller of our functions is not expected
// to be familiar with.
//
// Therefore we do some light transformation to get a more suitable
// error that should make more sense to our callers. These are
// still not awesome error messages, but at least they refer to
// the timestamp portions by name rather than by Go's example
// values.
if err.LayoutElem == "" && err.ValueElem == "" && err.Message != "" {
// For some reason err.Message is populated with a ": " prefix
// by the time package.
return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp%s", err.Message)
}
var what string
switch err.LayoutElem {
case "2006":
what = "year"
case "01":
what = "month"
case "02":
what = "day of month"
case "15":
what = "hour"
case "04":
what = "minute"
case "05":
what = "second"
case "Z07:00":
what = "UTC offset"
case "T":
return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp: missing required time introducer 'T'")
case ":", "-":
if err.ValueElem == "" {
return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp: end of string where %q is expected", err.LayoutElem)
} else {
return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp: found %q where %q is expected", err.ValueElem, err.LayoutElem)
}
default:
// Should never get here, because RFC3339 includes only the
// above portions.
what = "timestamp segment"
}
if err.ValueElem == "" {
return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp: end of string before %s", what)
} else {
switch {
case what == "hour" && strings.Contains(err.ValueElem, ":"):
return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp: hour must be between 0 and 23 inclusive")
case what == "hour" && len(err.ValueElem) != 2:
return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp: hour must have exactly two digits")
case what == "minute" && len(err.ValueElem) != 2:
return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp: minute must have exactly two digits")
default:
return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp: cannot use %q as %s", err.ValueElem, what)
}
}
}
return time.Time{}, err
}
return t, nil
}
// TimeAdd adds a duration to a timestamp, returning a new timestamp.
//
// In the HCL language, timestamps are conventionally represented as

View File

@ -0,0 +1,219 @@
package stdlib
import (
"errors"
"strconv"
"time"
)
// This file inlines some RFC3339 parsing code that was added to the Go standard
// library's "time" package during the Go 1.20 development period but then
// reverted prior to release to follow the Go proposals process first.
//
// Our goal is to support only valid RFC3339 strings regardless of what version
// of Go is being used, because the Go stdlib is just an implementation detail
// of the cty stdlib and so these functions should not very their behavior
// significantly due to being compiled against a different Go version.
//
// These inline copies of the code from upstream should likely stay here
// indefinitely even if functionality like this _is_ accepted in a later version
// of Go, because this now defines cty's definition of RFC3339 parsing as
// intentionally independent of Go's.
func parseStrictRFC3339(str string) (time.Time, error) {
t, ok := parseRFC3339(str)
if !ok {
// If parsing failed then we'll try to use time.Parse to gather up a
// helpful error object.
_, err := time.Parse(time.RFC3339, str)
if err != nil {
return time.Time{}, err
}
// The parse template syntax cannot correctly validate RFC 3339.
// Explicitly check for cases that Parse is unable to validate for.
// See https://go.dev/issue/54580.
num2 := func(str string) byte { return 10*(str[0]-'0') + (str[1] - '0') }
switch {
case str[len("2006-01-02T")+1] == ':': // hour must be two digits
return time.Time{}, &time.ParseError{
Layout: time.RFC3339,
Value: str,
LayoutElem: "15",
ValueElem: str[len("2006-01-02T"):][:1],
Message: ": hour must have two digits",
}
case str[len("2006-01-02T15:04:05")] == ',': // sub-second separator must be a period
return time.Time{}, &time.ParseError{
Layout: time.RFC3339,
Value: str,
LayoutElem: ".",
ValueElem: ",",
Message: ": sub-second separator must be a period",
}
case str[len(str)-1] != 'Z':
switch {
case num2(str[len(str)-len("07:00"):]) >= 24: // timezone hour must be in range
return time.Time{}, &time.ParseError{
Layout: time.RFC3339,
Value: str,
LayoutElem: "Z07:00",
ValueElem: str[len(str)-len("Z07:00"):],
Message: ": timezone hour out of range",
}
case num2(str[len(str)-len("00"):]) >= 60: // timezone minute must be in range
return time.Time{}, &time.ParseError{
Layout: time.RFC3339,
Value: str,
LayoutElem: "Z07:00",
ValueElem: str[len(str)-len("Z07:00"):],
Message: ": timezone minute out of range",
}
}
default: // unknown error; should not occur
return time.Time{}, &time.ParseError{
Layout: time.RFC3339,
Value: str,
LayoutElem: time.RFC3339,
ValueElem: str,
Message: "",
}
}
}
return t, nil
}
func parseRFC3339(s string) (time.Time, bool) {
// parseUint parses s as an unsigned decimal integer and
// verifies that it is within some range.
// If it is invalid or out-of-range,
// it sets ok to false and returns the min value.
ok := true
parseUint := func(s string, min, max int) (x int) {
for _, c := range []byte(s) {
if c < '0' || '9' < c {
ok = false
return min
}
x = x*10 + int(c) - '0'
}
if x < min || max < x {
ok = false
return min
}
return x
}
// Parse the date and time.
if len(s) < len("2006-01-02T15:04:05") {
return time.Time{}, false
}
year := parseUint(s[0:4], 0, 9999) // e.g., 2006
month := parseUint(s[5:7], 1, 12) // e.g., 01
day := parseUint(s[8:10], 1, daysIn(time.Month(month), year)) // e.g., 02
hour := parseUint(s[11:13], 0, 23) // e.g., 15
min := parseUint(s[14:16], 0, 59) // e.g., 04
sec := parseUint(s[17:19], 0, 59) // e.g., 05
if !ok || !(s[4] == '-' && s[7] == '-' && s[10] == 'T' && s[13] == ':' && s[16] == ':') {
return time.Time{}, false
}
s = s[19:]
// Parse the fractional second.
var nsec int
if len(s) >= 2 && s[0] == '.' && isDigit(s, 1) {
n := 2
for ; n < len(s) && isDigit(s, n); n++ {
}
nsec, _, _ = parseNanoseconds(s, n)
s = s[n:]
}
// Parse the time zone.
loc := time.UTC
if len(s) != 1 || s[0] != 'Z' {
if len(s) != len("-07:00") {
return time.Time{}, false
}
hr := parseUint(s[1:3], 0, 23) // e.g., 07
mm := parseUint(s[4:6], 0, 59) // e.g., 00
if !ok || !((s[0] == '-' || s[0] == '+') && s[3] == ':') {
return time.Time{}, false
}
zoneOffsetSecs := (hr*60 + mm) * 60
if s[0] == '-' {
zoneOffsetSecs = -zoneOffsetSecs
}
loc = time.FixedZone("", zoneOffsetSecs)
}
t := time.Date(year, time.Month(month), day, hour, min, sec, nsec, loc)
return t, true
}
func isDigit(s string, i int) bool {
if len(s) <= i {
return false
}
c := s[i]
return '0' <= c && c <= '9'
}
func parseNanoseconds(value string, nbytes int) (ns int, rangeErrString string, err error) {
if value[0] != '.' && value[0] != ',' {
err = errBadTimestamp
return
}
if nbytes > 10 {
value = value[:10]
nbytes = 10
}
if ns, err = strconv.Atoi(value[1:nbytes]); err != nil {
return
}
if ns < 0 {
rangeErrString = "fractional second"
return
}
// We need nanoseconds, which means scaling by the number
// of missing digits in the format, maximum length 10.
scaleDigits := 10 - nbytes
for i := 0; i < scaleDigits; i++ {
ns *= 10
}
return
}
// These are internal errors used by the date parsing code and are not ever
// returned by public functions.
var errBadTimestamp = errors.New("bad value for field")
// daysBefore[m] counts the number of days in a non-leap year
// before month m begins. There is an entry for m=12, counting
// the number of days before January of next year (365).
var daysBefore = [...]int32{
0,
31,
31 + 28,
31 + 28 + 31,
31 + 28 + 31 + 30,
31 + 28 + 31 + 30 + 31,
31 + 28 + 31 + 30 + 31 + 30,
31 + 28 + 31 + 30 + 31 + 30 + 31,
31 + 28 + 31 + 30 + 31 + 30 + 31 + 31,
31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30,
31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31,
31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31 + 30,
31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31 + 30 + 31,
}
func daysIn(m time.Month, year int) int {
if m == time.February && isLeap(year) {
return 29
}
return int(daysBefore[m] - daysBefore[m-1])
}
func isLeap(year int) bool {
return year%4 == 0 && (year%100 != 0 || year%400 == 0)
}

View File

@ -26,17 +26,27 @@ var FormatFunc = function.New(&function.Spec{
},
},
VarParam: &function.Parameter{
Name: "args",
Type: cty.DynamicPseudoType,
AllowNull: true,
Name: "args",
Type: cty.DynamicPseudoType,
AllowNull: true,
AllowUnknown: true,
},
Type: function.StaticReturnType(cty.String),
Type: function.StaticReturnType(cty.String),
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
for _, arg := range args[1:] {
if !arg.IsWhollyKnown() {
// We require all nested values to be known because the only
// thing we can do for a collection/structural type is print
// it as JSON and that requires it to be wholly known.
// However, we might be able to refine the result with a
// known prefix, if there are literal characters before the
// first formatting verb.
f := args[0].AsString()
if idx := strings.IndexByte(f, '%'); idx > 0 {
prefix := f[:idx]
return cty.UnknownVal(cty.String).Refine().StringPrefix(prefix).NewValue(), nil
}
return cty.UnknownVal(cty.String), nil
}
}
@ -59,7 +69,8 @@ var FormatListFunc = function.New(&function.Spec{
AllowNull: true,
AllowUnknown: true,
},
Type: function.StaticReturnType(cty.List(cty.String)),
Type: function.StaticReturnType(cty.List(cty.String)),
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
fmtVal := args[0]
args = args[1:]
@ -164,7 +175,7 @@ var FormatListFunc = function.New(&function.Spec{
// We require all nested values to be known because the only
// thing we can do for a collection/structural type is print
// it as JSON and that requires it to be wholly known.
ret = append(ret, cty.UnknownVal(cty.String))
ret = append(ret, cty.UnknownVal(cty.String).RefineNotNull())
continue Results
}
}

View File

@ -26,7 +26,8 @@ var EqualFunc = function.New(&function.Spec{
AllowNull: true,
},
},
Type: function.StaticReturnType(cty.Bool),
Type: function.StaticReturnType(cty.Bool),
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
return args[0].Equals(args[1]), nil
},
@ -50,7 +51,8 @@ var NotEqualFunc = function.New(&function.Spec{
AllowNull: true,
},
},
Type: function.StaticReturnType(cty.Bool),
Type: function.StaticReturnType(cty.Bool),
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
return args[0].Equals(args[1]).Not(), nil
},
@ -77,6 +79,7 @@ var CoalesceFunc = function.New(&function.Spec{
}
return retType, nil
},
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
for _, argVal := range args {
if !argVal.IsKnown() {
@ -92,6 +95,10 @@ var CoalesceFunc = function.New(&function.Spec{
},
})
func refineNonNull(b *cty.RefinementBuilder) *cty.RefinementBuilder {
return b.NotNull()
}
// Equal determines whether the two given values are equal, returning a
// bool value.
func Equal(a cty.Value, b cty.Value) (cty.Value, error) {

View File

@ -1,6 +1,10 @@
package stdlib
import (
"bytes"
"strings"
"unicode/utf8"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/function"
"github.com/zclconf/go-cty/cty/json"
@ -12,18 +16,40 @@ var JSONEncodeFunc = function.New(&function.Spec{
{
Name: "val",
Type: cty.DynamicPseudoType,
AllowUnknown: true,
AllowDynamicType: true,
AllowNull: true,
},
},
Type: function.StaticReturnType(cty.String),
Type: function.StaticReturnType(cty.String),
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
val := args[0]
if !val.IsWhollyKnown() {
// We can't serialize unknowns, so if the value is unknown or
// contains any _nested_ unknowns then our result must be
// unknown.
return cty.UnknownVal(retType), nil
// unknown. However, we might still be able to at least constrain
// the prefix of our string so that downstreams can sniff for
// whether it's valid JSON and what result types it could have.
valRng := val.Range()
if valRng.CouldBeNull() {
// If null is possible then we can't constrain the result
// beyond the type constraint, because the very first character
// of the string is what distinguishes a null.
return cty.UnknownVal(retType), nil
}
b := cty.UnknownVal(retType).Refine()
ty := valRng.TypeConstraint()
switch {
case ty == cty.String:
b = b.StringPrefixFull(`"`)
case ty.IsObjectType() || ty.IsMapType():
b = b.StringPrefixFull("{")
case ty.IsTupleType() || ty.IsListType() || ty.IsSetType():
b = b.StringPrefixFull("[")
}
return b.NewValue(), nil
}
if val.IsNull() {
@ -35,6 +61,11 @@ var JSONEncodeFunc = function.New(&function.Spec{
return cty.NilVal, err
}
// json.Marshal should already produce a trimmed string, but we'll
// make sure it always is because our unknown value refinements above
// assume there will be no leading whitespace before the value.
buf = bytes.TrimSpace(buf)
return cty.StringVal(string(buf)), nil
},
})
@ -50,6 +81,42 @@ var JSONDecodeFunc = function.New(&function.Spec{
Type: func(args []cty.Value) (cty.Type, error) {
str := args[0]
if !str.IsKnown() {
// If the string isn't known then we can't fully parse it, but
// if the value has been refined with a prefix then we may at
// least be able to reject obviously-invalid syntax and maybe
// even predict the result type. It's safe to return a specific
// result type only if parsing a full document with this prefix
// would return exactly that type or fail with a syntax error.
rng := str.Range()
if prefix := strings.TrimSpace(rng.StringPrefix()); prefix != "" {
// If we know at least one character then it should be one
// of the few characters that can introduce a JSON value.
switch r, _ := utf8.DecodeRuneInString(prefix); r {
case '{', '[':
// These can start object values and array values
// respectively, but we can't actually form a full
// object type constraint or tuple type constraint
// without knowing all of the attributes, so we
// will still return DynamicPseudoType in this case.
case '"':
// This means that the result will either be a string
// or parsing will fail.
return cty.String, nil
case 't', 'f':
// Must either be a boolean value or a syntax error.
return cty.Bool, nil
case '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.':
// These characters would all start the "number" production.
return cty.Number, nil
case 'n':
// n is valid to begin the keyword "null" but that doesn't
// give us any extra type information.
default:
// No other characters are valid as the beginning of a
// JSON value, so we can safely return an early error.
return cty.NilType, function.NewArgErrorf(0, "a JSON document cannot begin with the character %q", r)
}
}
return cty.DynamicPseudoType, nil
}

View File

@ -20,7 +20,8 @@ var AbsoluteFunc = function.New(&function.Spec{
AllowMarked: true,
},
},
Type: function.StaticReturnType(cty.Number),
Type: function.StaticReturnType(cty.Number),
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
return args[0].Absolute(), nil
},
@ -40,7 +41,8 @@ var AddFunc = function.New(&function.Spec{
AllowDynamicType: true,
},
},
Type: function.StaticReturnType(cty.Number),
Type: function.StaticReturnType(cty.Number),
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
// big.Float.Add can panic if the input values are opposing infinities,
// so we must catch that here in order to remain within
@ -74,7 +76,8 @@ var SubtractFunc = function.New(&function.Spec{
AllowDynamicType: true,
},
},
Type: function.StaticReturnType(cty.Number),
Type: function.StaticReturnType(cty.Number),
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
// big.Float.Sub can panic if the input values are infinities,
// so we must catch that here in order to remain within
@ -108,7 +111,8 @@ var MultiplyFunc = function.New(&function.Spec{
AllowDynamicType: true,
},
},
Type: function.StaticReturnType(cty.Number),
Type: function.StaticReturnType(cty.Number),
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
// big.Float.Mul can panic if the input values are both zero or both
// infinity, so we must catch that here in order to remain within
@ -143,7 +147,8 @@ var DivideFunc = function.New(&function.Spec{
AllowDynamicType: true,
},
},
Type: function.StaticReturnType(cty.Number),
Type: function.StaticReturnType(cty.Number),
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
// big.Float.Quo can panic if the input values are both zero or both
// infinity, so we must catch that here in order to remain within
@ -178,7 +183,8 @@ var ModuloFunc = function.New(&function.Spec{
AllowDynamicType: true,
},
},
Type: function.StaticReturnType(cty.Number),
Type: function.StaticReturnType(cty.Number),
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
// big.Float.Mul can panic if the input values are both zero or both
// infinity, so we must catch that here in order to remain within
@ -205,17 +211,20 @@ var GreaterThanFunc = function.New(&function.Spec{
{
Name: "a",
Type: cty.Number,
AllowUnknown: true,
AllowDynamicType: true,
AllowMarked: true,
},
{
Name: "b",
Type: cty.Number,
AllowUnknown: true,
AllowDynamicType: true,
AllowMarked: true,
},
},
Type: function.StaticReturnType(cty.Bool),
Type: function.StaticReturnType(cty.Bool),
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
return args[0].GreaterThan(args[1]), nil
},
@ -227,17 +236,20 @@ var GreaterThanOrEqualToFunc = function.New(&function.Spec{
{
Name: "a",
Type: cty.Number,
AllowUnknown: true,
AllowDynamicType: true,
AllowMarked: true,
},
{
Name: "b",
Type: cty.Number,
AllowUnknown: true,
AllowDynamicType: true,
AllowMarked: true,
},
},
Type: function.StaticReturnType(cty.Bool),
Type: function.StaticReturnType(cty.Bool),
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
return args[0].GreaterThanOrEqualTo(args[1]), nil
},
@ -249,17 +261,20 @@ var LessThanFunc = function.New(&function.Spec{
{
Name: "a",
Type: cty.Number,
AllowUnknown: true,
AllowDynamicType: true,
AllowMarked: true,
},
{
Name: "b",
Type: cty.Number,
AllowUnknown: true,
AllowDynamicType: true,
AllowMarked: true,
},
},
Type: function.StaticReturnType(cty.Bool),
Type: function.StaticReturnType(cty.Bool),
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
return args[0].LessThan(args[1]), nil
},
@ -271,17 +286,20 @@ var LessThanOrEqualToFunc = function.New(&function.Spec{
{
Name: "a",
Type: cty.Number,
AllowUnknown: true,
AllowDynamicType: true,
AllowMarked: true,
},
{
Name: "b",
Type: cty.Number,
AllowUnknown: true,
AllowDynamicType: true,
AllowMarked: true,
},
},
Type: function.StaticReturnType(cty.Bool),
Type: function.StaticReturnType(cty.Bool),
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
return args[0].LessThanOrEqualTo(args[1]), nil
},
@ -297,7 +315,8 @@ var NegateFunc = function.New(&function.Spec{
AllowMarked: true,
},
},
Type: function.StaticReturnType(cty.Number),
Type: function.StaticReturnType(cty.Number),
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
return args[0].Negate(), nil
},
@ -311,7 +330,8 @@ var MinFunc = function.New(&function.Spec{
Type: cty.Number,
AllowDynamicType: true,
},
Type: function.StaticReturnType(cty.Number),
Type: function.StaticReturnType(cty.Number),
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
if len(args) == 0 {
return cty.NilVal, fmt.Errorf("must pass at least one number")
@ -336,7 +356,8 @@ var MaxFunc = function.New(&function.Spec{
Type: cty.Number,
AllowDynamicType: true,
},
Type: function.StaticReturnType(cty.Number),
Type: function.StaticReturnType(cty.Number),
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
if len(args) == 0 {
return cty.NilVal, fmt.Errorf("must pass at least one number")
@ -362,7 +383,8 @@ var IntFunc = function.New(&function.Spec{
AllowDynamicType: true,
},
},
Type: function.StaticReturnType(cty.Number),
Type: function.StaticReturnType(cty.Number),
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
bf := args[0].AsBigFloat()
if bf.IsInt() {
@ -384,7 +406,8 @@ var CeilFunc = function.New(&function.Spec{
Type: cty.Number,
},
},
Type: function.StaticReturnType(cty.Number),
Type: function.StaticReturnType(cty.Number),
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
f := args[0].AsBigFloat()
@ -414,7 +437,8 @@ var FloorFunc = function.New(&function.Spec{
Type: cty.Number,
},
},
Type: function.StaticReturnType(cty.Number),
Type: function.StaticReturnType(cty.Number),
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
f := args[0].AsBigFloat()
@ -447,7 +471,8 @@ var LogFunc = function.New(&function.Spec{
Type: cty.Number,
},
},
Type: function.StaticReturnType(cty.Number),
Type: function.StaticReturnType(cty.Number),
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
var num float64
if err := gocty.FromCtyValue(args[0], &num); err != nil {
@ -476,7 +501,8 @@ var PowFunc = function.New(&function.Spec{
Type: cty.Number,
},
},
Type: function.StaticReturnType(cty.Number),
Type: function.StaticReturnType(cty.Number),
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
var num float64
if err := gocty.FromCtyValue(args[0], &num); err != nil {
@ -502,7 +528,8 @@ var SignumFunc = function.New(&function.Spec{
Type: cty.Number,
},
},
Type: function.StaticReturnType(cty.Number),
Type: function.StaticReturnType(cty.Number),
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
var num int
if err := gocty.FromCtyValue(args[0], &num); err != nil {
@ -539,6 +566,7 @@ var ParseIntFunc = function.New(&function.Spec{
}
return cty.Number, nil
},
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
var numstr string

View File

@ -33,6 +33,7 @@ var RegexFunc = function.New(&function.Spec{
}
return retTy, err
},
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
if retType == cty.DynamicPseudoType {
return cty.DynamicVal, nil
@ -79,6 +80,7 @@ var RegexAllFunc = function.New(&function.Spec{
}
return cty.List(retTy), err
},
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
ety := retType.ElementType()
if ety == cty.DynamicPseudoType {

View File

@ -74,6 +74,7 @@ var ConcatFunc = function.New(&function.Spec{
}
return cty.Tuple(etys), nil
},
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
switch {
case retType.IsListType():
@ -143,7 +144,8 @@ var RangeFunc = function.New(&function.Spec{
Name: "params",
Type: cty.Number,
},
Type: function.StaticReturnType(cty.List(cty.Number)),
Type: function.StaticReturnType(cty.List(cty.Number)),
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
var start, end, step cty.Value
switch len(args) {

View File

@ -23,7 +23,8 @@ var SetHasElementFunc = function.New(&function.Spec{
AllowDynamicType: true,
},
},
Type: function.StaticReturnType(cty.Bool),
Type: function.StaticReturnType(cty.Bool),
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
return args[0].HasElement(args[1]), nil
},
@ -43,7 +44,8 @@ var SetUnionFunc = function.New(&function.Spec{
Type: cty.Set(cty.DynamicPseudoType),
AllowDynamicType: true,
},
Type: setOperationReturnType,
Type: setOperationReturnType,
RefineResult: refineNonNull,
Impl: setOperationImpl(func(s1, s2 cty.ValueSet) cty.ValueSet {
return s1.Union(s2)
}, true),
@ -63,7 +65,8 @@ var SetIntersectionFunc = function.New(&function.Spec{
Type: cty.Set(cty.DynamicPseudoType),
AllowDynamicType: true,
},
Type: setOperationReturnType,
Type: setOperationReturnType,
RefineResult: refineNonNull,
Impl: setOperationImpl(func(s1, s2 cty.ValueSet) cty.ValueSet {
return s1.Intersection(s2)
}, false),
@ -83,7 +86,8 @@ var SetSubtractFunc = function.New(&function.Spec{
AllowDynamicType: true,
},
},
Type: setOperationReturnType,
Type: setOperationReturnType,
RefineResult: refineNonNull,
Impl: setOperationImpl(func(s1, s2 cty.ValueSet) cty.ValueSet {
return s1.Subtract(s2)
}, false),
@ -103,7 +107,8 @@ var SetSymmetricDifferenceFunc = function.New(&function.Spec{
Type: cty.Set(cty.DynamicPseudoType),
AllowDynamicType: true,
},
Type: setOperationReturnType,
Type: setOperationReturnType,
RefineResult: refineNonNull,
Impl: setOperationImpl(func(s1, s2 cty.ValueSet) cty.ValueSet {
return s1.SymmetricDifference(s2)
}, false),

View File

@ -22,7 +22,8 @@ var UpperFunc = function.New(&function.Spec{
AllowDynamicType: true,
},
},
Type: function.StaticReturnType(cty.String),
Type: function.StaticReturnType(cty.String),
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
in := args[0].AsString()
out := strings.ToUpper(in)
@ -39,7 +40,8 @@ var LowerFunc = function.New(&function.Spec{
AllowDynamicType: true,
},
},
Type: function.StaticReturnType(cty.String),
Type: function.StaticReturnType(cty.String),
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
in := args[0].AsString()
out := strings.ToLower(in)
@ -56,7 +58,8 @@ var ReverseFunc = function.New(&function.Spec{
AllowDynamicType: true,
},
},
Type: function.StaticReturnType(cty.String),
Type: function.StaticReturnType(cty.String),
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
in := []byte(args[0].AsString())
out := make([]byte, len(in))
@ -81,25 +84,46 @@ var StrlenFunc = function.New(&function.Spec{
{
Name: "str",
Type: cty.String,
AllowUnknown: true,
AllowDynamicType: true,
},
},
Type: function.StaticReturnType(cty.Number),
RefineResult: func(b *cty.RefinementBuilder) *cty.RefinementBuilder {
// String length is never null and never negative.
// (We might refine the lower bound even more inside Impl.)
return b.NotNull().NumberRangeLowerBound(cty.NumberIntVal(0), true)
},
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
in := args[0].AsString()
l := 0
inB := []byte(in)
for i := 0; i < len(in); {
d, _, _ := textseg.ScanGraphemeClusters(inB[i:], true)
l++
i += d
if !args[0].IsKnown() {
ret := cty.UnknownVal(cty.Number)
// We may be able to still return a constrained result based on the
// refined range of the unknown value.
inRng := args[0].Range()
if inRng.TypeConstraint() == cty.String {
prefixLen := int64(graphemeClusterCount(inRng.StringPrefix()))
ret = ret.Refine().NumberRangeLowerBound(cty.NumberIntVal(prefixLen), true).NewValue()
}
return ret, nil
}
in := args[0].AsString()
l := graphemeClusterCount(in)
return cty.NumberIntVal(int64(l)), nil
},
})
func graphemeClusterCount(in string) int {
l := 0
inB := []byte(in)
for i := 0; i < len(in); {
d, _, _ := textseg.ScanGraphemeClusters(inB[i:], true)
l++
i += d
}
return l
}
var SubstrFunc = function.New(&function.Spec{
Description: "Extracts a substring from the given string.",
Params: []function.Parameter{
@ -122,7 +146,8 @@ var SubstrFunc = function.New(&function.Spec{
AllowDynamicType: true,
},
},
Type: function.StaticReturnType(cty.String),
Type: function.StaticReturnType(cty.String),
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
in := []byte(args[0].AsString())
var offset, length int
@ -218,7 +243,8 @@ var JoinFunc = function.New(&function.Spec{
Description: "One or more lists of strings to join.",
Type: cty.List(cty.String),
},
Type: function.StaticReturnType(cty.String),
Type: function.StaticReturnType(cty.String),
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
sep := args[0].AsString()
listVals := args[1:]
@ -258,18 +284,29 @@ var SortFunc = function.New(&function.Spec{
Description: "Applies a lexicographic sort to the elements of the given list.",
Params: []function.Parameter{
{
Name: "list",
Type: cty.List(cty.String),
Name: "list",
Type: cty.List(cty.String),
AllowUnknown: true,
},
},
Type: function.StaticReturnType(cty.List(cty.String)),
Type: function.StaticReturnType(cty.List(cty.String)),
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
listVal := args[0]
if !listVal.IsWhollyKnown() {
// If some of the element values aren't known yet then we
// can't yet predict the order of the result.
return cty.UnknownVal(retType), nil
// can't yet predict the order of the result, but we can be
// sure that the length won't change.
ret := cty.UnknownVal(retType)
if listVal.Type().IsListType() {
rng := listVal.Range()
ret = ret.Refine().
CollectionLengthLowerBound(rng.LengthLowerBound()).
CollectionLengthUpperBound(rng.LengthUpperBound()).
NewValue()
}
return ret, nil
}
if listVal.LengthInt() == 0 { // Easy path
return listVal, nil
@ -307,7 +344,8 @@ var SplitFunc = function.New(&function.Spec{
Type: cty.String,
},
},
Type: function.StaticReturnType(cty.List(cty.String)),
Type: function.StaticReturnType(cty.List(cty.String)),
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
sep := args[0].AsString()
str := args[1].AsString()
@ -333,7 +371,8 @@ var ChompFunc = function.New(&function.Spec{
Type: cty.String,
},
},
Type: function.StaticReturnType(cty.String),
Type: function.StaticReturnType(cty.String),
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
newlines := regexp.MustCompile(`(?:\r\n?|\n)*\z`)
return cty.StringVal(newlines.ReplaceAllString(args[0].AsString(), "")), nil
@ -356,7 +395,8 @@ var IndentFunc = function.New(&function.Spec{
Type: cty.String,
},
},
Type: function.StaticReturnType(cty.String),
Type: function.StaticReturnType(cty.String),
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
var spaces int
if err := gocty.FromCtyValue(args[0], &spaces); err != nil {
@ -378,7 +418,8 @@ var TitleFunc = function.New(&function.Spec{
Type: cty.String,
},
},
Type: function.StaticReturnType(cty.String),
Type: function.StaticReturnType(cty.String),
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
return cty.StringVal(strings.Title(args[0].AsString())), nil
},
@ -394,7 +435,8 @@ var TrimSpaceFunc = function.New(&function.Spec{
Type: cty.String,
},
},
Type: function.StaticReturnType(cty.String),
Type: function.StaticReturnType(cty.String),
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
return cty.StringVal(strings.TrimSpace(args[0].AsString())), nil
},
@ -416,7 +458,8 @@ var TrimFunc = function.New(&function.Spec{
Type: cty.String,
},
},
Type: function.StaticReturnType(cty.String),
Type: function.StaticReturnType(cty.String),
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
str := args[0].AsString()
cutset := args[1].AsString()
@ -443,7 +486,8 @@ var TrimPrefixFunc = function.New(&function.Spec{
Type: cty.String,
},
},
Type: function.StaticReturnType(cty.String),
Type: function.StaticReturnType(cty.String),
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
str := args[0].AsString()
prefix := args[1].AsString()
@ -467,7 +511,8 @@ var TrimSuffixFunc = function.New(&function.Spec{
Type: cty.String,
},
},
Type: function.StaticReturnType(cty.String),
Type: function.StaticReturnType(cty.String),
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
str := args[0].AsString()
cutset := args[1].AsString()

View File

@ -30,7 +30,8 @@ var ReplaceFunc = function.New(&function.Spec{
Type: cty.String,
},
},
Type: function.StaticReturnType(cty.String),
Type: function.StaticReturnType(cty.String),
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
str := args[0].AsString()
substr := args[1].AsString()
@ -59,7 +60,8 @@ var RegexReplaceFunc = function.New(&function.Spec{
Type: cty.String,
},
},
Type: function.StaticReturnType(cty.String),
Type: function.StaticReturnType(cty.String),
RefineResult: refineNonNull,
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
str := args[0].AsString()
substr := args[1].AsString()

View File

@ -8,7 +8,7 @@ import (
// unknowns, for operations that short-circuit to return unknown in that case.
func anyUnknown(values ...Value) bool {
for _, val := range values {
if val.v == unknown {
if _, unknown := val.v.(*unknownType); unknown {
return true
}
}
@ -39,7 +39,7 @@ func typeCheck(required Type, ret Type, values ...Value) (shortCircuit *Value, e
)
}
if val.v == unknown {
if _, unknown := val.v.(*unknownType); unknown {
hasUnknown = true
}
}

View File

@ -190,6 +190,9 @@ func (val Value) HasSameMarks(other Value) bool {
// An application that never calls this method does not need to worry about
// handling marked values.
func (val Value) Mark(mark interface{}) Value {
if _, ok := mark.(ValueMarks); ok {
panic("cannot call Value.Mark with a ValueMarks value (use WithMarks instead)")
}
var newMarker marker
newMarker.realV = val.v
if mr, ok := val.v.(marker); ok {

View File

@ -3,11 +3,19 @@ package cty
// unknownType is the placeholder type used for the sigil value representing
// "Unknown", to make it unambigiously distinct from any other possible value.
type unknownType struct {
// refinement is an optional object which, if present, describes some
// additional constraints we know about the range of real values this
// unknown value could be a placeholder for.
refinement unknownValRefinement
}
// unknown is a special value that can be used as the internal value of a
// Value to create a placeholder for a value that isn't yet known.
var unknown interface{} = &unknownType{}
// totallyUnknown is the representation a a value we know nothing about at
// all. Subsequent refinements of an unknown value will cause creation of
// other values of unknownType that can represent additional constraints
// on the unknown value, but all unknown values start as totally unknown
// and we will also typically lose all unknown value refinements when
// round-tripping through serialization formats.
var totallyUnknown interface{} = &unknownType{}
// UnknownVal returns an Value that represents an unknown value of the given
// type. Unknown values can be used to represent a value that is
@ -19,7 +27,7 @@ var unknown interface{} = &unknownType{}
func UnknownVal(t Type) Value {
return Value{
ty: t,
v: unknown,
v: totallyUnknown,
}
}
@ -80,6 +88,6 @@ func init() {
}
DynamicVal = Value{
ty: DynamicPseudoType,
v: unknown,
v: totallyUnknown,
}
}

View File

@ -0,0 +1,747 @@
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

View File

@ -48,7 +48,8 @@ func (val Value) IsKnown() bool {
if val.IsMarked() {
return val.unmarkForce().IsKnown()
}
return val.v != unknown
_, unknown := val.v.(*unknownType)
return !unknown
}
// IsNull returns true if the value is null. Values of any type can be

View File

@ -5,8 +5,7 @@ import (
"math/big"
"reflect"
"golang.org/x/text/unicode/norm"
"github.com/zclconf/go-cty/cty/ctystrings"
"github.com/zclconf/go-cty/cty/set"
)
@ -107,7 +106,7 @@ func StringVal(v string) Value {
// A return value from this function can be meaningfully compared byte-for-byte
// with a Value.AsString result.
func NormalizeString(s string) string {
return norm.NFC.String(s)
return ctystrings.Normalize(s)
}
// ObjectVal returns a Value of an object type whose structure is defined

View File

@ -33,7 +33,17 @@ func (val Value) GoString() string {
return "cty.DynamicVal"
}
if !val.IsKnown() {
return fmt.Sprintf("cty.UnknownVal(%#v)", val.ty)
rfn := val.v.(*unknownType).refinement
var suffix string
if rfn != nil {
calls := rfn.GoString()
if calls == ".NotNull()" {
suffix = ".RefineNotNull()"
} else {
suffix = ".Refine()" + rfn.GoString() + ".NewValue()"
}
}
return fmt.Sprintf("cty.UnknownVal(%#v)%s", val.ty, suffix)
}
// By the time we reach here we've dealt with all of the exceptions around
@ -125,13 +135,38 @@ func (val Value) Equals(other Value) Value {
return val.Equals(other).WithMarks(valMarks, otherMarks)
}
// Start by handling Unknown values before considering types.
// This needs to be done since Null values are always equal regardless of
// type.
// Some easy cases with comparisons to null.
switch {
case val.IsNull() && definitelyNotNull(other):
return False
case other.IsNull() && definitelyNotNull(val):
return False
}
// If we have one known value and one unknown value then we may be
// able to quickly disqualify equality based on the range of the unknown
// value.
if val.IsKnown() && !other.IsKnown() {
otherRng := other.Range()
if ok := otherRng.Includes(val); ok.IsKnown() && ok.False() {
return False
}
} else if other.IsKnown() && !val.IsKnown() {
valRng := val.Range()
if ok := valRng.Includes(other); ok.IsKnown() && ok.False() {
return False
}
}
// We need to deal with unknown values before anything else with nulls
// because any unknown value that hasn't yet been refined as non-null
// could become null, and nulls of any types are equal to one another.
unknownResult := func() Value {
return UnknownVal(Bool).Refine().NotNull().NewValue()
}
switch {
case !val.IsKnown() && !other.IsKnown():
// both unknown
return UnknownVal(Bool)
return unknownResult()
case val.IsKnown() && !other.IsKnown():
switch {
case val.IsNull(), other.ty.HasDynamicTypes():
@ -139,13 +174,13 @@ func (val Value) Equals(other Value) Value {
// nulls of any type are equal.
// An unknown with a dynamic type compares as unknown, which we need
// to check before the type comparison below.
return UnknownVal(Bool)
return unknownResult()
case !val.ty.Equals(other.ty):
// There is no null comparison or dynamic types, so unequal types
// will never be equal.
return False
default:
return UnknownVal(Bool)
return unknownResult()
}
case other.IsKnown() && !val.IsKnown():
switch {
@ -154,13 +189,13 @@ func (val Value) Equals(other Value) Value {
// nulls of any type are equal.
// An unknown with a dynamic type compares as unknown, which we need
// to check before the type comparison below.
return UnknownVal(Bool)
return unknownResult()
case !other.ty.Equals(val.ty):
// There's no null comparison or dynamic types, so unequal types
// will never be equal.
return False
default:
return UnknownVal(Bool)
return unknownResult()
}
}
@ -182,7 +217,7 @@ func (val Value) Equals(other Value) Value {
return BoolVal(false)
}
return UnknownVal(Bool)
return unknownResult()
}
if !val.ty.Equals(other.ty) {
@ -216,7 +251,7 @@ func (val Value) Equals(other Value) Value {
}
eq := lhs.Equals(rhs)
if !eq.IsKnown() {
return UnknownVal(Bool)
return unknownResult()
}
if eq.False() {
result = false
@ -237,7 +272,7 @@ func (val Value) Equals(other Value) Value {
}
eq := lhs.Equals(rhs)
if !eq.IsKnown() {
return UnknownVal(Bool)
return unknownResult()
}
if eq.False() {
result = false
@ -259,7 +294,7 @@ func (val Value) Equals(other Value) Value {
}
eq := lhs.Equals(rhs)
if !eq.IsKnown() {
return UnknownVal(Bool)
return unknownResult()
}
if eq.False() {
result = false
@ -276,8 +311,8 @@ func (val Value) Equals(other Value) Value {
// in one are also in the other.
for it := s1.Iterator(); it.Next(); {
rv := it.Value()
if rv == unknown { // "unknown" is the internal representation of unknown-ness
return UnknownVal(Bool)
if _, unknown := rv.(*unknownType); unknown { // "*unknownType" is the internal representation of unknown-ness
return unknownResult()
}
if !s2.Has(rv) {
equal = false
@ -285,8 +320,8 @@ func (val Value) Equals(other Value) Value {
}
for it := s2.Iterator(); it.Next(); {
rv := it.Value()
if rv == unknown { // "unknown" is the internal representation of unknown-ness
return UnknownVal(Bool)
if _, unknown := rv.(*unknownType); unknown { // "*unknownType" is the internal representation of unknown-ness
return unknownResult()
}
if !s1.Has(rv) {
equal = false
@ -313,7 +348,7 @@ func (val Value) Equals(other Value) Value {
}
eq := lhs.Equals(rhs)
if !eq.IsKnown() {
return UnknownVal(Bool)
return unknownResult()
}
if eq.False() {
result = false
@ -393,7 +428,17 @@ func (val Value) RawEquals(other Value) bool {
other = other.unmarkForce()
if (!val.IsKnown()) && (!other.IsKnown()) {
return true
// If either unknown value has refinements then they must match.
valRfn := val.v.(*unknownType).refinement
otherRfn := other.v.(*unknownType).refinement
switch {
case (valRfn == nil) != (otherRfn == nil):
return false
case valRfn != nil:
return valRfn.rawEqual(otherRfn)
default:
return true
}
}
if (val.IsKnown() && !other.IsKnown()) || (other.IsKnown() && !val.IsKnown()) {
return false
@ -548,7 +593,8 @@ func (val Value) Add(other Value) Value {
if shortCircuit := mustTypeCheck(Number, Number, val, other); shortCircuit != nil {
shortCircuit = forceShortCircuitType(shortCircuit, Number)
return *shortCircuit
ret := shortCircuit.RefineWith(numericRangeArithmetic(Value.Add, val.Range(), other.Range()))
return ret.RefineNotNull()
}
ret := new(big.Float)
@ -567,7 +613,8 @@ func (val Value) Subtract(other Value) Value {
if shortCircuit := mustTypeCheck(Number, Number, val, other); shortCircuit != nil {
shortCircuit = forceShortCircuitType(shortCircuit, Number)
return *shortCircuit
ret := shortCircuit.RefineWith(numericRangeArithmetic(Value.Subtract, val.Range(), other.Range()))
return ret.RefineNotNull()
}
return val.Add(other.Negate())
@ -583,7 +630,7 @@ func (val Value) Negate() Value {
if shortCircuit := mustTypeCheck(Number, Number, val); shortCircuit != nil {
shortCircuit = forceShortCircuitType(shortCircuit, Number)
return *shortCircuit
return (*shortCircuit).RefineNotNull()
}
ret := new(big.Float).Neg(val.v.(*big.Float))
@ -600,8 +647,14 @@ func (val Value) Multiply(other Value) Value {
}
if shortCircuit := mustTypeCheck(Number, Number, val, other); shortCircuit != nil {
// If either value is exactly zero then the result must either be
// zero or an error.
if val == Zero || other == Zero {
return Zero
}
shortCircuit = forceShortCircuitType(shortCircuit, Number)
return *shortCircuit
ret := shortCircuit.RefineWith(numericRangeArithmetic(Value.Multiply, val.Range(), other.Range()))
return ret.RefineNotNull()
}
// find the larger precision of the arguments
@ -646,7 +699,10 @@ func (val Value) Divide(other Value) Value {
if shortCircuit := mustTypeCheck(Number, Number, val, other); shortCircuit != nil {
shortCircuit = forceShortCircuitType(shortCircuit, Number)
return *shortCircuit
// TODO: We could potentially refine the range of the result here, but
// we don't right now because our division operation is not monotone
// if the denominator could potentially be zero.
return (*shortCircuit).RefineNotNull()
}
ret := new(big.Float)
@ -678,7 +734,7 @@ func (val Value) Modulo(other Value) Value {
if shortCircuit := mustTypeCheck(Number, Number, val, other); shortCircuit != nil {
shortCircuit = forceShortCircuitType(shortCircuit, Number)
return *shortCircuit
return (*shortCircuit).RefineNotNull()
}
// We cheat a bit here with infinities, just abusing the Multiply operation
@ -716,7 +772,7 @@ func (val Value) Absolute() Value {
if shortCircuit := mustTypeCheck(Number, Number, val); shortCircuit != nil {
shortCircuit = forceShortCircuitType(shortCircuit, Number)
return *shortCircuit
return (*shortCircuit).Refine().NotNull().NumberRangeInclusive(Zero, UnknownVal(Number)).NewValue()
}
ret := (&big.Float{}).Abs(val.v.(*big.Float))
@ -889,23 +945,23 @@ func (val Value) HasIndex(key Value) Value {
}
if val.ty == DynamicPseudoType {
return UnknownVal(Bool)
return UnknownVal(Bool).RefineNotNull()
}
switch {
case val.Type().IsListType():
if key.Type() == DynamicPseudoType {
return UnknownVal(Bool)
return UnknownVal(Bool).RefineNotNull()
}
if key.Type() != Number {
return False
}
if !key.IsKnown() {
return UnknownVal(Bool)
return UnknownVal(Bool).RefineNotNull()
}
if !val.IsKnown() {
return UnknownVal(Bool)
return UnknownVal(Bool).RefineNotNull()
}
index, accuracy := key.v.(*big.Float).Int64()
@ -916,17 +972,17 @@ func (val Value) HasIndex(key Value) Value {
return BoolVal(int(index) < len(val.v.([]interface{})) && index >= 0)
case val.Type().IsMapType():
if key.Type() == DynamicPseudoType {
return UnknownVal(Bool)
return UnknownVal(Bool).RefineNotNull()
}
if key.Type() != String {
return False
}
if !key.IsKnown() {
return UnknownVal(Bool)
return UnknownVal(Bool).RefineNotNull()
}
if !val.IsKnown() {
return UnknownVal(Bool)
return UnknownVal(Bool).RefineNotNull()
}
keyStr := key.v.(string)
@ -935,14 +991,14 @@ func (val Value) HasIndex(key Value) Value {
return BoolVal(exists)
case val.Type().IsTupleType():
if key.Type() == DynamicPseudoType {
return UnknownVal(Bool)
return UnknownVal(Bool).RefineNotNull()
}
if key.Type() != Number {
return False
}
if !key.IsKnown() {
return UnknownVal(Bool)
return UnknownVal(Bool).RefineNotNull()
}
index, accuracy := key.v.(*big.Float).Int64()
@ -977,10 +1033,10 @@ func (val Value) HasElement(elem Value) Value {
panic("not a set type")
}
if !val.IsKnown() || !elem.IsKnown() {
return UnknownVal(Bool)
return UnknownVal(Bool).RefineNotNull()
}
if val.IsNull() {
panic("can't call HasElement on a nil value")
panic("can't call HasElement on a null value")
}
if !ty.ElementType().Equals(elem.Type()) {
return False
@ -1012,7 +1068,10 @@ func (val Value) Length() Value {
}
if !val.IsKnown() {
return UnknownVal(Number)
// If the whole collection isn't known then the length isn't known
// either, but we can still put some bounds on the range of the result.
rng := val.Range()
return UnknownVal(Number).RefineWith(valueRefineLengthResult(rng))
}
if val.Type().IsSetType() {
// The Length rules are a little different for sets because if any
@ -1030,13 +1089,26 @@ func (val Value) Length() Value {
// unknown value cannot represent more than one known value.
return NumberIntVal(storeLength)
}
// Otherwise, we cannot predict the length.
return UnknownVal(Number)
// Otherwise, we cannot predict the length exactly but we can at
// least constrain both bounds of its range, because value coalescing
// can only ever reduce the number of elements in the set.
return UnknownVal(Number).Refine().NotNull().NumberRangeInclusive(NumberIntVal(1), NumberIntVal(storeLength)).NewValue()
}
return NumberIntVal(int64(val.LengthInt()))
}
func valueRefineLengthResult(collRng ValueRange) func(*RefinementBuilder) *RefinementBuilder {
return func(b *RefinementBuilder) *RefinementBuilder {
return b.
NotNull().
NumberRangeInclusive(
NumberIntVal(int64(collRng.LengthLowerBound())),
NumberIntVal(int64(collRng.LengthUpperBound())),
)
}
}
// LengthInt is like Length except it returns an int. It has the same behavior
// as Length except that it will panic if the receiver is unknown.
//
@ -1167,7 +1239,7 @@ func (val Value) Not() Value {
if shortCircuit := mustTypeCheck(Bool, Bool, val); shortCircuit != nil {
shortCircuit = forceShortCircuitType(shortCircuit, Bool)
return *shortCircuit
return (*shortCircuit).RefineNotNull()
}
return BoolVal(!val.v.(bool))
@ -1183,8 +1255,14 @@ func (val Value) And(other Value) Value {
}
if shortCircuit := mustTypeCheck(Bool, Bool, val, other); shortCircuit != nil {
// If either value is known to be exactly False then it doesn't
// matter what the other value is, because the final result must
// either be False or an error.
if val == False || other == False {
return False
}
shortCircuit = forceShortCircuitType(shortCircuit, Bool)
return *shortCircuit
return (*shortCircuit).RefineNotNull()
}
return BoolVal(val.v.(bool) && other.v.(bool))
@ -1200,8 +1278,14 @@ func (val Value) Or(other Value) Value {
}
if shortCircuit := mustTypeCheck(Bool, Bool, val, other); shortCircuit != nil {
// If either value is known to be exactly True then it doesn't
// matter what the other value is, because the final result must
// either be True or an error.
if val == True || other == True {
return True
}
shortCircuit = forceShortCircuitType(shortCircuit, Bool)
return *shortCircuit
return (*shortCircuit).RefineNotNull()
}
return BoolVal(val.v.(bool) || other.v.(bool))
@ -1217,8 +1301,30 @@ func (val Value) LessThan(other Value) Value {
}
if shortCircuit := mustTypeCheck(Number, Bool, val, other); shortCircuit != nil {
// We might be able to return a known answer even with unknown inputs.
// FIXME: This is more conservative than it needs to be, because it
// treats all bounds as exclusive bounds.
valRng := val.Range()
otherRng := other.Range()
if valRng.TypeConstraint() == Number && other.Range().TypeConstraint() == Number {
valMax, _ := valRng.NumberUpperBound()
otherMin, _ := otherRng.NumberLowerBound()
if valMax.IsKnown() && otherMin.IsKnown() {
if r := valMax.LessThan(otherMin); r.True() {
return True
}
}
valMin, _ := valRng.NumberLowerBound()
otherMax, _ := otherRng.NumberUpperBound()
if valMin.IsKnown() && otherMax.IsKnown() {
if r := valMin.GreaterThan(otherMax); r.True() {
return False
}
}
}
shortCircuit = forceShortCircuitType(shortCircuit, Bool)
return *shortCircuit
return (*shortCircuit).RefineNotNull()
}
return BoolVal(val.v.(*big.Float).Cmp(other.v.(*big.Float)) < 0)
@ -1234,8 +1340,30 @@ func (val Value) GreaterThan(other Value) Value {
}
if shortCircuit := mustTypeCheck(Number, Bool, val, other); shortCircuit != nil {
// We might be able to return a known answer even with unknown inputs.
// FIXME: This is more conservative than it needs to be, because it
// treats all bounds as exclusive bounds.
valRng := val.Range()
otherRng := other.Range()
if valRng.TypeConstraint() == Number && other.Range().TypeConstraint() == Number {
valMin, _ := valRng.NumberLowerBound()
otherMax, _ := otherRng.NumberUpperBound()
if valMin.IsKnown() && otherMax.IsKnown() {
if r := valMin.GreaterThan(otherMax); r.True() {
return True
}
}
valMax, _ := valRng.NumberUpperBound()
otherMin, _ := otherRng.NumberLowerBound()
if valMax.IsKnown() && otherMin.IsKnown() {
if r := valMax.LessThan(otherMin); r.True() {
return False
}
}
}
shortCircuit = forceShortCircuitType(shortCircuit, Bool)
return *shortCircuit
return (*shortCircuit).RefineNotNull()
}
return BoolVal(val.v.(*big.Float).Cmp(other.v.(*big.Float)) > 0)

408
vendor/github.com/zclconf/go-cty/cty/value_range.go generated vendored Normal file
View File

@ -0,0 +1,408 @@
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()
}