Skip to content

Commit

Permalink
function/stdlib: FormatFunc and FormatListFunc can handle DynamicPseu…
Browse files Browse the repository at this point in the history
…doType

We didn't previously include AllowDynamicType: true on the variadic
parameters for these functions and so the function system assumed that
this code couldn't deal with unspecified types at all.

However, the formatter is actually written to tolerate all possible values
and do _something_ reasonable with all of them, so we can safely opt in to
handle dynamic-typed arguments ourselves.

In particular that means that `cty.NullVal(cty.DynamicPseudoType)` is now
treated the same way as null values of known types, and `cty.DynamicVal`
is treated the same way as unknown values of known types.
  • Loading branch information
apparentlymart committed Jan 11, 2025
1 parent 6edebd2 commit 8920baa
Show file tree
Hide file tree
Showing 3 changed files with 132 additions and 43 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# 1.16.1 (Unreleased)

* `function/stdlib`: `FormatFunc` and `FormatListFunc` now handle unknown and null values of unknown type as arguments, rather than letting the function system's short-circuit behavior take care of it. This allows `cty.DynamicVal` and `cty.NullVal(cty.DynamicPseudoType)` to be treated consistently with other values, returning results consistent with the documented behavior, rather than forcing the function to immediately return `cty.DynamicVal`.

# 1.16.0 (January 3, 2025)

Expand Down
80 changes: 41 additions & 39 deletions cty/function/stdlib/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,11 @@ var FormatFunc = function.New(&function.Spec{
},
},
VarParam: &function.Parameter{
Name: "args",
Type: cty.DynamicPseudoType,
AllowNull: true,
AllowUnknown: true,
Name: "args",
Type: cty.DynamicPseudoType,
AllowNull: true,
AllowUnknown: true,
AllowDynamicType: true,
},
Type: function.StaticReturnType(cty.String),
RefineResult: refineNonNull,
Expand Down Expand Up @@ -64,10 +65,11 @@ var FormatListFunc = function.New(&function.Spec{
},
},
VarParam: &function.Parameter{
Name: "args",
Type: cty.DynamicPseudoType,
AllowNull: true,
AllowUnknown: true,
Name: "args",
Type: cty.DynamicPseudoType,
AllowNull: true,
AllowUnknown: true,
AllowDynamicType: true,
},
Type: function.StaticReturnType(cty.List(cty.String)),
RefineResult: refineNonNull,
Expand Down Expand Up @@ -199,32 +201,32 @@ var FormatListFunc = function.New(&function.Spec{
//
// It supports the following "verbs":
//
// %% Literal percent sign, consuming no value
// %v A default formatting of the value based on type, as described below.
// %#v JSON serialization of the value
// %t Converts to boolean and then produces "true" or "false"
// %b Converts to number, requires integer, produces binary representation
// %d Converts to number, requires integer, produces decimal representation
// %o Converts to number, requires integer, produces octal representation
// %x Converts to number, requires integer, produces hexadecimal representation
// with lowercase letters
// %X Like %x but with uppercase letters
// %e Converts to number, produces scientific notation like -1.234456e+78
// %E Like %e but with an uppercase "E" representing the exponent
// %f Converts to number, produces decimal representation with fractional
// part but no exponent, like 123.456
// %g %e for large exponents or %f otherwise
// %G %E for large exponents or %f otherwise
// %s Converts to string and produces the string's characters
// %q Converts to string and produces JSON-quoted string representation,
// like %v.
// %% Literal percent sign, consuming no value
// %v A default formatting of the value based on type, as described below.
// %#v JSON serialization of the value
// %t Converts to boolean and then produces "true" or "false"
// %b Converts to number, requires integer, produces binary representation
// %d Converts to number, requires integer, produces decimal representation
// %o Converts to number, requires integer, produces octal representation
// %x Converts to number, requires integer, produces hexadecimal representation
// with lowercase letters
// %X Like %x but with uppercase letters
// %e Converts to number, produces scientific notation like -1.234456e+78
// %E Like %e but with an uppercase "E" representing the exponent
// %f Converts to number, produces decimal representation with fractional
// part but no exponent, like 123.456
// %g %e for large exponents or %f otherwise
// %G %E for large exponents or %f otherwise
// %s Converts to string and produces the string's characters
// %q Converts to string and produces JSON-quoted string representation,
// like %v.
//
// The default format selections made by %v are:
//
// string %s
// number %g
// bool %t
// other %#v
// string %s
// number %g
// bool %t
// other %#v
//
// Null values produce the literal keyword "null" for %v and %#v, and produce
// an error otherwise.
Expand All @@ -236,10 +238,10 @@ var FormatListFunc = function.New(&function.Spec{
// is used. A period with no following number is invalid.
// For examples:
//
// %f default width, default precision
// %9f width 9, default precision
// %.2f default width, precision 2
// %9.2f width 9, precision 2
// %f default width, default precision
// %9f width 9, default precision
// %.2f default width, precision 2
// %9.2f width 9, precision 2
//
// Width and precision are measured in unicode characters (grapheme clusters).
//
Expand All @@ -256,10 +258,10 @@ var FormatListFunc = function.New(&function.Spec{
// The following additional symbols can be used immediately after the percent
// introducer as flags:
//
// (a space) leave a space where the sign would be if number is positive
// + Include a sign for a number even if it is positive (numeric only)
// - Pad with spaces on the left rather than the right
// 0 Pad with zeros rather than spaces.
// (a space) leave a space where the sign would be if number is positive
// + Include a sign for a number even if it is positive (numeric only)
// - Pad with spaces on the left rather than the right
// 0 Pad with zeros rather than spaces.
//
// Flag characters are ignored for verbs that do not support them.
//
Expand Down
94 changes: 90 additions & 4 deletions cty/function/stdlib/format_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,12 @@ func TestFormat(t *testing.T) {
cty.StringVal("null"),
``,
},
{
cty.StringVal("%v"),
[]cty.Value{cty.NullVal(cty.DynamicPseudoType)},
cty.StringVal("null"),
``,
},

// Strings
{
Expand Down Expand Up @@ -152,6 +158,12 @@ func TestFormat(t *testing.T) {
cty.NilVal,
`unsupported value for "%s" at 0: null value cannot be formatted`,
},
{
cty.StringVal("%s"),
[]cty.Value{cty.NullVal(cty.DynamicPseudoType)},
cty.NilVal,
`unsupported value for "%s" at 0: null value cannot be formatted`,
},
{
cty.StringVal("%10s"),
[]cty.Value{cty.StringVal("hello")},
Expand Down Expand Up @@ -274,6 +286,18 @@ func TestFormat(t *testing.T) {
cty.StringVal("This statement is false"),
``,
},
{
cty.StringVal("This statement is %t"),
[]cty.Value{cty.NullVal(cty.Bool)},
cty.NilVal,
`unsupported value for "%t" at 18: null value cannot be formatted`,
},
{
cty.StringVal("This statement is %t"),
[]cty.Value{cty.NullVal(cty.DynamicPseudoType)},
cty.NilVal,
`unsupported value for "%t" at 18: null value cannot be formatted`,
},

// Integer Numbers
{
Expand Down Expand Up @@ -318,6 +342,24 @@ func TestFormat(t *testing.T) {
cty.NilVal,
`unsupported value for "%d" at 0: number required`,
},
{
cty.StringVal("%d green bottles standing on the wall"),
[]cty.Value{cty.NullVal(cty.Number)},
cty.NilVal,
`unsupported value for "%d" at 0: null value cannot be formatted`,
},
{
cty.StringVal("%d green bottles standing on the wall"),
[]cty.Value{cty.NullVal(cty.EmptyTuple)},
cty.NilVal,
`unsupported value for "%d" at 0: null value cannot be formatted`,
},
{
cty.StringVal("%d green bottles standing on the wall"),
[]cty.Value{cty.NullVal(cty.DynamicPseudoType)},
cty.NilVal,
`unsupported value for "%d" at 0: null value cannot be formatted`,
},
{
cty.StringVal("%b"),
[]cty.Value{cty.NumberIntVal(5)},
Expand Down Expand Up @@ -481,6 +523,12 @@ func TestFormat(t *testing.T) {
cty.UnknownVal(cty.String).RefineNotNull(),
``,
},
{
cty.StringVal("%v"),
[]cty.Value{cty.DynamicVal},
cty.UnknownVal(cty.String).RefineNotNull(),
``,
},

// Invalids
{
Expand Down Expand Up @@ -556,6 +604,7 @@ func TestFormat(t *testing.T) {
`too many arguments; only 1 used by format string`,
},

// Marked values
{
cty.StringVal("hello %s").Mark(1),
[]cty.Value{cty.StringVal("world")},
Expand Down Expand Up @@ -854,10 +903,47 @@ func TestFormatList(t *testing.T) {
23: {
cty.StringVal("%v"),
[]cty.Value{cty.DynamicVal},
// The current Function implementation will default to DynamicVal
// if AllowUnknown is true, even though this function has a static
// return type
cty.DynamicVal,
cty.UnknownVal(cty.List(cty.String)).RefineNotNull(),
``,
},
24: {
cty.StringVal("%v"),
[]cty.Value{cty.NullVal(cty.DynamicPseudoType)},
cty.ListVal([]cty.Value{
cty.StringVal("null"),
}),
``,
},
25: {
cty.StringVal("%v %v"),
[]cty.Value{
cty.NullVal(cty.DynamicPseudoType),
cty.ListVal([]cty.Value{
cty.StringVal("a"),
cty.NullVal(cty.String),
cty.StringVal("c"),
}),
},
cty.ListVal([]cty.Value{
cty.StringVal("null a"),
cty.StringVal("null null"),
cty.StringVal("null c"),
}),
``,
},
26: {
cty.StringVal("%v %v"),
[]cty.Value{
cty.NullVal(cty.DynamicPseudoType),
cty.ListVal([]cty.Value{
cty.NullVal(cty.DynamicPseudoType),
cty.NullVal(cty.DynamicPseudoType),
}),
},
cty.ListVal([]cty.Value{
cty.StringVal("null null"),
cty.StringVal("null null"),
}),
``,
},
}
Expand Down

0 comments on commit 8920baa

Please sign in to comment.