mirror of
https://github.com/opentofu/opentofu.git
synced 2025-01-27 00:46:25 -06:00
lang/funcs: startswith considers string prefix refinement
If the string to be tested is an unknown value that's been refined with a prefix and the prefix we're being asked to test is in turn a prefix of that known prefix then we can return a known answer despite the inputs not being fully known. There are also some other similar deductions we can make about other combinations of inputs. This extra analysis could be useful in a custom condition check that requires a string with a particular prefix, since it can allow the condition to fail even on partially-unknown input, thereby giving earlier feedback about a problem.
This commit is contained in:
parent
691018dd00
commit
81c15f987e
@ -16,8 +16,9 @@ import (
|
||||
var StartsWithFunc = function.New(&function.Spec{
|
||||
Params: []function.Parameter{
|
||||
{
|
||||
Name: "str",
|
||||
Type: cty.String,
|
||||
Name: "str",
|
||||
Type: cty.String,
|
||||
AllowUnknown: true,
|
||||
},
|
||||
{
|
||||
Name: "prefix",
|
||||
@ -27,9 +28,31 @@ var StartsWithFunc = function.New(&function.Spec{
|
||||
Type: function.StaticReturnType(cty.Bool),
|
||||
RefineResult: refineNotNull,
|
||||
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
|
||||
str := args[0].AsString()
|
||||
prefix := args[1].AsString()
|
||||
|
||||
if !args[0].IsKnown() {
|
||||
// If the unknown value has a known prefix then we might be
|
||||
// able to still produce a known result.
|
||||
if prefix == "" {
|
||||
// The empty string is a prefix of any string.
|
||||
return cty.True, nil
|
||||
}
|
||||
if knownPrefix := args[0].Range().StringPrefix(); knownPrefix != "" {
|
||||
if strings.HasPrefix(knownPrefix, prefix) {
|
||||
return cty.True, nil
|
||||
}
|
||||
if len(knownPrefix) >= len(prefix) {
|
||||
// If the prefix we're testing is no longer than the known
|
||||
// prefix and it didn't match then the full string with
|
||||
// that same prefix can't match either.
|
||||
return cty.False, nil
|
||||
}
|
||||
}
|
||||
return cty.UnknownVal(cty.Bool), nil
|
||||
}
|
||||
|
||||
str := args[0].AsString()
|
||||
|
||||
if strings.HasPrefix(str, prefix) {
|
||||
return cty.True, nil
|
||||
}
|
||||
@ -104,12 +127,6 @@ var ReplaceFunc = function.New(&function.Spec{
|
||||
},
|
||||
})
|
||||
|
||||
// Replace searches a given string for another given substring,
|
||||
// and replaces all occurences with a given replacement string.
|
||||
func Replace(str, substr, replace cty.Value) (cty.Value, error) {
|
||||
return ReplaceFunc.Call([]cty.Value{str, substr, replace})
|
||||
}
|
||||
|
||||
// StrContainsFunc searches a given string for another given substring,
|
||||
// if found the function returns true, otherwise returns false.
|
||||
var StrContainsFunc = function.New(&function.Spec{
|
||||
@ -135,3 +152,13 @@ var StrContainsFunc = function.New(&function.Spec{
|
||||
return cty.False, nil
|
||||
},
|
||||
})
|
||||
|
||||
// Replace searches a given string for another given substring,
|
||||
// and replaces all occurences with a given replacement string.
|
||||
func Replace(str, substr, replace cty.Value) (cty.Value, error) {
|
||||
return ReplaceFunc.Call([]cty.Value{str, substr, replace})
|
||||
}
|
||||
|
||||
func StrContains(str, substr cty.Value) (cty.Value, error) {
|
||||
return StrContainsFunc.Call([]cty.Value{str, substr})
|
||||
}
|
||||
|
@ -134,6 +134,122 @@ func TestStrContains(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func StrContains(str, substr cty.Value) (cty.Value, error) {
|
||||
return StrContainsFunc.Call([]cty.Value{str, substr})
|
||||
func TestStartsWith(t *testing.T) {
|
||||
tests := []struct {
|
||||
String, Prefix cty.Value
|
||||
Want cty.Value
|
||||
WantError string
|
||||
}{
|
||||
{
|
||||
cty.StringVal("hello world"),
|
||||
cty.StringVal("hello"),
|
||||
cty.True,
|
||||
``,
|
||||
},
|
||||
{
|
||||
cty.StringVal("hey world"),
|
||||
cty.StringVal("hello"),
|
||||
cty.False,
|
||||
``,
|
||||
},
|
||||
{
|
||||
cty.StringVal(""),
|
||||
cty.StringVal(""),
|
||||
cty.True,
|
||||
``,
|
||||
},
|
||||
{
|
||||
cty.StringVal("a"),
|
||||
cty.StringVal(""),
|
||||
cty.True,
|
||||
``,
|
||||
},
|
||||
{
|
||||
cty.StringVal(""),
|
||||
cty.StringVal("a"),
|
||||
cty.False,
|
||||
``,
|
||||
},
|
||||
{
|
||||
cty.UnknownVal(cty.String),
|
||||
cty.StringVal("a"),
|
||||
cty.UnknownVal(cty.Bool).RefineNotNull(),
|
||||
``,
|
||||
},
|
||||
{
|
||||
cty.UnknownVal(cty.String),
|
||||
cty.StringVal(""),
|
||||
cty.True,
|
||||
``,
|
||||
},
|
||||
{
|
||||
cty.UnknownVal(cty.String).Refine().StringPrefix("https:").NewValue(),
|
||||
cty.StringVal(""),
|
||||
cty.True,
|
||||
``,
|
||||
},
|
||||
{
|
||||
cty.UnknownVal(cty.String).Refine().StringPrefix("https:").NewValue(),
|
||||
cty.StringVal("a"),
|
||||
cty.False,
|
||||
``,
|
||||
},
|
||||
{
|
||||
cty.UnknownVal(cty.String).Refine().StringPrefix("https:").NewValue(),
|
||||
cty.StringVal("ht"),
|
||||
cty.True,
|
||||
``,
|
||||
},
|
||||
{
|
||||
cty.UnknownVal(cty.String).Refine().StringPrefix("https:").NewValue(),
|
||||
cty.StringVal("https:"),
|
||||
cty.True,
|
||||
``,
|
||||
},
|
||||
{
|
||||
cty.UnknownVal(cty.String).Refine().StringPrefix("https:").NewValue(),
|
||||
cty.StringVal("https-"),
|
||||
cty.False,
|
||||
``,
|
||||
},
|
||||
{
|
||||
cty.UnknownVal(cty.String).Refine().StringPrefix("https:").NewValue(),
|
||||
cty.StringVal("https://"),
|
||||
cty.UnknownVal(cty.Bool).RefineNotNull(),
|
||||
``,
|
||||
},
|
||||
{
|
||||
// Unicode combining characters edge-case: we match the prefix
|
||||
// in terms of unicode code units rather than grapheme clusters,
|
||||
// which is inconsistent with our string processing elsewhere but
|
||||
// would be a breaking change to fix that bug now.
|
||||
cty.StringVal("\U0001f937\u200d\u2642"), // "Man Shrugging" is encoded as "Person Shrugging" followed by zero-width joiner and then the masculine gender presentation modifier
|
||||
cty.StringVal("\U0001f937"), // Just the "Person Shrugging" character without any modifiers
|
||||
cty.True,
|
||||
``,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(fmt.Sprintf("StartsWith(%#v, %#v)", test.String, test.Prefix), func(t *testing.T) {
|
||||
got, err := StartsWithFunc.Call([]cty.Value{test.String, test.Prefix})
|
||||
|
||||
if test.WantError != "" {
|
||||
gotErr := fmt.Sprintf("%s", err)
|
||||
if gotErr != test.WantError {
|
||||
t.Errorf("wrong error\ngot: %s\nwant: %s", gotErr, test.WantError)
|
||||
}
|
||||
return
|
||||
} else if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
|
||||
if !got.RawEquals(test.Want) {
|
||||
t.Errorf(
|
||||
"wrong result\nstring: %#v\nprefix: %#v\ngot: %#v\nwant: %#v",
|
||||
test.String, test.Prefix, got, test.Want,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user