Skip to content

Commit b868a8d

Browse files
cty: Silently ignore refinements of cty.DynamicVal
For historical reasons we cannot permit any refinements on cty.DynamicVal, because existing callers expect that (marks notwithstanding) cty.DynamicVal is the only possible unknown value of an unknown type, and so adding refinements would invalidate that assumption. In the initial implementation of refinements it was treated as a caller programming error to try to refine cty.DynamicVal. However, that violates the convention that cty.DynamicVal is usable as a broad placeholder value that supports any operation that would be valid on at least one possible value. Instead, we'll now compromise by just silently ignoring any attempt to refine cty.DynamicVal. The refinement operation will run to completion without panicking but it will also completely ignore all of the refinement builder calls and eventually just return another plain cty.DynamicVal as the "refined" result. This is consistent with our typical expectation that refinements are propagated on a best-effort basis but can be silently discarded (or lose some detail) when passing through operations that don't or can't support them. This means that it should now be safe to call Refine on any value, but refining can still panic if the specific refinements selected don't make sense for whatever value is being refined. This also includes some direct tests of the Value.Refine API, since it was previously only tested indirectly through other operations that consume or manipulate refinements.
1 parent ab81272 commit b868a8d

File tree

4 files changed

+568
-15
lines changed

4 files changed

+568
-15
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# 1.14.1 (Unreleased)
22

3+
* `cty`: It's now valid to use the `Refine` method on `cty.DynamicVal`, although all refinements will be silently discarded. This replaces the original behavior of panicking when trying to refine `cty.DynamicVal`.
34

45
# 1.14.0 (August 30, 2023)
56

cty/unknown_refinement.go

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,17 @@ func (v Value) Refine() *RefinementBuilder {
4444
var wip unknownValRefinement
4545
switch {
4646
case ty == DynamicPseudoType && !v.IsKnown():
47-
panic("cannot refine an unknown value of an unknown type")
47+
// This case specifically matches DynamicVal, which is constrained
48+
// by backward compatibility to be a singleton and so we cannot allow
49+
// any refinements to it.
50+
// To preserve the typical assumption that DynamicVal is a safe
51+
// placeholder to use when no value is known at all, we silently
52+
// ignore all attempts to refine this particular value and just
53+
// always echo back a totally-unrefined DynamicVal.
54+
return &RefinementBuilder{
55+
orig: DynamicVal,
56+
marks: marks,
57+
}
4858
case ty == String:
4959
wip = &refinementString{}
5060
case ty == Number:
@@ -136,10 +146,27 @@ type RefinementBuilder struct {
136146
wip unknownValRefinement
137147
}
138148

139-
func (b *RefinementBuilder) assertRefineable() {
149+
// refineable is an internal detail to help with two special situations
150+
// related to refinements:
151+
// - If the refinement is to a value of a type that doesn't support any
152+
// refinements at all, this function will immediately panic with a
153+
// message reporting that, because it's a caller bug to try to refine
154+
// a value in a way that's inappropriate for its known type.
155+
// - If the refinement is to an unknown value of an unknown type
156+
// (i.e. cty.DynamicVal) then it returns false, indicating that the
157+
// caller should just silently ignore whatever refinement was requested.
158+
// - In all other cases this function returns true, which means the direct
159+
// caller should attempt to apply the requested refinement, and then
160+
// panic itself if the requested refinement doesn't make sense for the
161+
// specific value being refined.
162+
func (b *RefinementBuilder) refineable() bool {
163+
if b.orig == DynamicVal {
164+
return false
165+
}
140166
if b.wip == nil {
141167
panic(fmt.Sprintf("cannot refine a %#v value", b.orig.Type()))
142168
}
169+
return true
143170
}
144171

145172
// NotNull constrains the value as definitely not being null.
@@ -157,7 +184,9 @@ func (b *RefinementBuilder) assertRefineable() {
157184
// -- a value whose type is `cty.DynamicPseudoType` -- as being non-null.
158185
// An unknown value of an unknown type is always completely unconstrained.
159186
func (b *RefinementBuilder) NotNull() *RefinementBuilder {
160-
b.assertRefineable()
187+
if !b.refineable() {
188+
return b
189+
}
161190

162191
if b.orig.IsKnown() && b.orig.IsNull() {
163192
panic("refining null value as non-null")
@@ -181,7 +210,9 @@ func (b *RefinementBuilder) NotNull() *RefinementBuilder {
181210
// value for each type constraint -- but this is here for symmetry with the
182211
// fact that a [ValueRange] can also represent that a value is definitely null.
183212
func (b *RefinementBuilder) Null() *RefinementBuilder {
184-
b.assertRefineable()
213+
if !b.refineable() {
214+
return b
215+
}
185216

186217
if b.orig.IsKnown() && !b.orig.IsNull() {
187218
panic("refining non-null value as null")
@@ -209,7 +240,9 @@ func (b *RefinementBuilder) NumberRangeInclusive(min, max Value) *RefinementBuil
209240
// NumberRangeLowerBound constraints the lower bound of a number value, or
210241
// panics if this builder is not refining a number value.
211242
func (b *RefinementBuilder) NumberRangeLowerBound(min Value, inclusive bool) *RefinementBuilder {
212-
b.assertRefineable()
243+
if !b.refineable() {
244+
return b
245+
}
213246

214247
wip, ok := b.wip.(*refinementNumber)
215248
if !ok {
@@ -258,7 +291,9 @@ func (b *RefinementBuilder) NumberRangeLowerBound(min Value, inclusive bool) *Re
258291
// NumberRangeUpperBound constraints the upper bound of a number value, or
259292
// panics if this builder is not refining a number value.
260293
func (b *RefinementBuilder) NumberRangeUpperBound(max Value, inclusive bool) *RefinementBuilder {
261-
b.assertRefineable()
294+
if !b.refineable() {
295+
return b
296+
}
262297

263298
wip, ok := b.wip.(*refinementNumber)
264299
if !ok {
@@ -308,7 +343,9 @@ func (b *RefinementBuilder) NumberRangeUpperBound(max Value, inclusive bool) *Re
308343
// collection value, or panics if this builder is not refining a collection
309344
// value.
310345
func (b *RefinementBuilder) CollectionLengthLowerBound(min int) *RefinementBuilder {
311-
b.assertRefineable()
346+
if !b.refineable() {
347+
return b
348+
}
312349

313350
wip, ok := b.wip.(*refinementCollection)
314351
if !ok {
@@ -340,7 +377,9 @@ func (b *RefinementBuilder) CollectionLengthLowerBound(min int) *RefinementBuild
340377
// The upper bound must be a known, non-null number or this function will
341378
// panic.
342379
func (b *RefinementBuilder) CollectionLengthUpperBound(max int) *RefinementBuilder {
343-
b.assertRefineable()
380+
if !b.refineable() {
381+
return b
382+
}
344383

345384
wip, ok := b.wip.(*refinementCollection)
346385
if !ok {
@@ -419,7 +458,9 @@ func (b *RefinementBuilder) StringPrefix(prefix string) *RefinementBuilder {
419458
// Use [RefinementBuilder.StringPrefix] instead if an application cannot fully
420459
// control the final result to avoid violating this rule.
421460
func (b *RefinementBuilder) StringPrefixFull(prefix string) *RefinementBuilder {
422-
b.assertRefineable()
461+
if !b.refineable() {
462+
return b
463+
}
423464

424465
wip, ok := b.wip.(*refinementString)
425466
if !ok {
@@ -487,7 +528,7 @@ func (b *RefinementBuilder) NewValue() (ret Value) {
487528
ret = ret.WithMarks(b.marks)
488529
}()
489530

490-
if b.orig.IsKnown() {
531+
if b.orig.IsKnown() || b.orig == DynamicVal {
491532
return b.orig
492533
}
493534

0 commit comments

Comments
 (0)