2021-09-30 03:11:56 +00:00
|
|
|
package pcl
|
2020-05-11 18:17:36 +00:00
|
|
|
|
|
|
|
import (
|
2021-06-24 16:17:55 +00:00
|
|
|
"strings"
|
|
|
|
|
2020-05-11 18:17:36 +00:00
|
|
|
"github.com/hashicorp/hcl/v2"
|
2020-05-29 10:38:53 +00:00
|
|
|
"github.com/hashicorp/hcl/v2/hclsyntax"
|
2021-03-17 13:20:05 +00:00
|
|
|
"github.com/pulumi/pulumi/pkg/v3/codegen"
|
|
|
|
"github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/model"
|
|
|
|
"github.com/pulumi/pulumi/pkg/v3/codegen/schema"
|
|
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
|
2020-05-11 18:17:36 +00:00
|
|
|
"github.com/zclconf/go-cty/cty"
|
2020-05-29 10:38:53 +00:00
|
|
|
"github.com/zclconf/go-cty/cty/convert"
|
2020-05-11 18:17:36 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
func sameSchemaTypes(xt, yt model.Type) bool {
|
|
|
|
xs, _ := GetSchemaForType(xt)
|
|
|
|
ys, _ := GetSchemaForType(yt)
|
|
|
|
|
|
|
|
if xs == ys {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
xu, ok := xs.(*schema.UnionType)
|
|
|
|
if !ok {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
yu, ok := ys.(*schema.UnionType)
|
|
|
|
if !ok {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
types := codegen.Set{}
|
|
|
|
for _, t := range xu.ElementTypes {
|
|
|
|
types.Add(t)
|
|
|
|
}
|
|
|
|
for _, t := range yu.ElementTypes {
|
|
|
|
if !types.Has(t) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2020-06-02 19:00:35 +00:00
|
|
|
// rewriteConversions implements the core of RewriteConversions. It returns the rewritten expression and true if the
|
|
|
|
// type of the expression may have changed.
|
2022-07-07 20:15:47 +00:00
|
|
|
func rewriteConversions(x model.Expression, to model.Type, diags *hcl.Diagnostics) (model.Expression, bool) {
|
2023-05-02 14:07:58 +00:00
|
|
|
if x == nil || to == nil {
|
|
|
|
return x, false
|
2022-12-16 12:37:08 +00:00
|
|
|
}
|
2020-06-02 19:00:35 +00:00
|
|
|
// If rewriting an operand changed its type and the type of the expression depends on the type of that operand, the
|
|
|
|
// expression must be typechecked in order to update its type.
|
|
|
|
var typecheck bool
|
|
|
|
|
2020-05-11 18:17:36 +00:00
|
|
|
switch x := x.(type) {
|
2020-05-13 15:25:26 +00:00
|
|
|
case *model.AnonymousFunctionExpression:
|
2022-07-07 20:15:47 +00:00
|
|
|
x.Body, _ = rewriteConversions(x.Body, to, diags)
|
2020-05-11 18:17:36 +00:00
|
|
|
case *model.BinaryOpExpression:
|
2022-07-07 20:15:47 +00:00
|
|
|
x.LeftOperand, _ = rewriteConversions(x.LeftOperand, model.InputType(x.LeftOperandType()), diags)
|
|
|
|
x.RightOperand, _ = rewriteConversions(x.RightOperand, model.InputType(x.RightOperandType()), diags)
|
2020-05-11 18:17:36 +00:00
|
|
|
case *model.ConditionalExpression:
|
2020-06-02 19:00:35 +00:00
|
|
|
var trueChanged, falseChanged bool
|
2022-07-07 20:15:47 +00:00
|
|
|
x.Condition, _ = rewriteConversions(x.Condition, model.InputType(model.BoolType), diags)
|
|
|
|
x.TrueResult, trueChanged = rewriteConversions(x.TrueResult, to, diags)
|
|
|
|
x.FalseResult, falseChanged = rewriteConversions(x.FalseResult, to, diags)
|
2020-06-02 19:00:35 +00:00
|
|
|
typecheck = trueChanged || falseChanged
|
2020-05-11 18:17:36 +00:00
|
|
|
case *model.ForExpression:
|
2020-05-13 15:25:26 +00:00
|
|
|
traverserType := model.NumberType
|
2020-05-11 18:17:36 +00:00
|
|
|
if x.Key != nil {
|
2020-05-13 15:25:26 +00:00
|
|
|
traverserType = model.StringType
|
2022-07-07 20:15:47 +00:00
|
|
|
x.Key, _ = rewriteConversions(x.Key, model.InputType(model.StringType), diags)
|
2020-05-11 18:17:36 +00:00
|
|
|
}
|
|
|
|
if x.Condition != nil {
|
2022-07-07 20:15:47 +00:00
|
|
|
x.Condition, _ = rewriteConversions(x.Condition, model.InputType(model.BoolType), diags)
|
2020-05-11 18:17:36 +00:00
|
|
|
}
|
2020-05-13 15:25:26 +00:00
|
|
|
|
2022-07-07 20:15:47 +00:00
|
|
|
valueType, tdiags := to.Traverse(model.MakeTraverser(traverserType))
|
|
|
|
*diags = diags.Extend(tdiags)
|
2020-05-13 15:25:26 +00:00
|
|
|
|
2022-07-07 20:15:47 +00:00
|
|
|
x.Value, typecheck = rewriteConversions(x.Value, valueType.(model.Type), diags)
|
2020-05-11 18:17:36 +00:00
|
|
|
case *model.FunctionCallExpression:
|
|
|
|
args := x.Args
|
|
|
|
for _, param := range x.Signature.Parameters {
|
|
|
|
if len(args) == 0 {
|
|
|
|
break
|
|
|
|
}
|
2022-07-07 20:15:47 +00:00
|
|
|
args[0], _ = rewriteConversions(args[0], model.InputType(param.Type), diags)
|
2020-05-11 18:17:36 +00:00
|
|
|
args = args[1:]
|
|
|
|
}
|
|
|
|
if x.Signature.VarargsParameter != nil {
|
|
|
|
for i := range args {
|
2022-07-07 20:15:47 +00:00
|
|
|
args[i], _ = rewriteConversions(args[i], model.InputType(x.Signature.VarargsParameter.Type), diags)
|
2020-05-11 18:17:36 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
case *model.IndexExpression:
|
2022-07-07 20:15:47 +00:00
|
|
|
x.Key, _ = rewriteConversions(x.Key, x.KeyType(), diags)
|
2020-05-11 18:17:36 +00:00
|
|
|
case *model.ObjectConsExpression:
|
2021-04-16 12:04:42 +00:00
|
|
|
if v := resolveDiscriminatedUnions(x, to); v != nil {
|
|
|
|
to = v
|
|
|
|
typecheck = true
|
|
|
|
}
|
2020-05-11 18:17:36 +00:00
|
|
|
for i := range x.Items {
|
|
|
|
item := &x.Items[i]
|
2022-11-15 01:00:09 +00:00
|
|
|
if item.Key.Type() == model.DynamicType {
|
|
|
|
// We don't know the type of this expression, so we can't correct the
|
|
|
|
// type.
|
|
|
|
continue
|
|
|
|
}
|
2020-05-11 18:17:36 +00:00
|
|
|
|
2022-07-07 20:15:47 +00:00
|
|
|
key, ediags := item.Key.Evaluate(&hcl.EvalContext{}) // empty context, we need a constant string
|
|
|
|
*diags = diags.Extend(ediags)
|
|
|
|
|
2022-11-15 01:00:09 +00:00
|
|
|
valueType, tdiags := to.Traverse(hcl.TraverseIndex{
|
|
|
|
Key: key,
|
|
|
|
SrcRange: item.Key.SyntaxNode().Range(),
|
|
|
|
})
|
2022-07-07 20:15:47 +00:00
|
|
|
*diags = diags.Extend(tdiags)
|
2020-05-11 18:17:36 +00:00
|
|
|
|
2020-06-02 19:00:35 +00:00
|
|
|
var valueChanged bool
|
2022-07-07 20:15:47 +00:00
|
|
|
item.Key, _ = rewriteConversions(item.Key, model.InputType(model.StringType), diags)
|
|
|
|
item.Value, valueChanged = rewriteConversions(item.Value, valueType.(model.Type), diags)
|
2020-06-02 19:00:35 +00:00
|
|
|
typecheck = typecheck || valueChanged
|
2020-05-11 18:17:36 +00:00
|
|
|
}
|
|
|
|
case *model.TupleConsExpression:
|
2022-11-15 01:00:09 +00:00
|
|
|
for i, expr := range x.Expressions {
|
|
|
|
if expr.Type() == model.DynamicType {
|
|
|
|
// We don't know the type of this expression, so we can't correct the
|
|
|
|
// type.
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
valueType, tdiags := to.Traverse(hcl.TraverseIndex{
|
|
|
|
Key: cty.NumberIntVal(int64(i)),
|
|
|
|
SrcRange: x.Syntax.Range(),
|
|
|
|
})
|
2022-07-07 20:15:47 +00:00
|
|
|
*diags = diags.Extend(tdiags)
|
2020-05-11 18:17:36 +00:00
|
|
|
|
2020-06-02 19:00:35 +00:00
|
|
|
var exprChanged bool
|
2022-11-15 01:00:09 +00:00
|
|
|
x.Expressions[i], exprChanged = rewriteConversions(expr, valueType.(model.Type), diags)
|
2020-06-02 19:00:35 +00:00
|
|
|
typecheck = typecheck || exprChanged
|
2020-05-11 18:17:36 +00:00
|
|
|
}
|
|
|
|
case *model.UnaryOpExpression:
|
2022-07-07 20:15:47 +00:00
|
|
|
x.Operand, _ = rewriteConversions(x.Operand, model.InputType(x.OperandType()), diags)
|
2020-05-11 18:17:36 +00:00
|
|
|
}
|
|
|
|
|
2020-06-02 19:00:35 +00:00
|
|
|
var typeChanged bool
|
|
|
|
if typecheck {
|
2022-07-07 20:15:47 +00:00
|
|
|
typecheckDiags := x.Typecheck(false)
|
|
|
|
*diags = diags.Extend(typecheckDiags)
|
2020-06-02 19:00:35 +00:00
|
|
|
typeChanged = true
|
2020-05-11 18:17:36 +00:00
|
|
|
}
|
2020-06-02 19:00:35 +00:00
|
|
|
|
2020-05-29 10:38:53 +00:00
|
|
|
// If we can convert a primitive value in place, do so.
|
|
|
|
if value, ok := convertPrimitiveValues(x, to); ok {
|
2020-06-02 19:00:35 +00:00
|
|
|
x, typeChanged = value, true
|
|
|
|
}
|
|
|
|
// If the expression's type is directly assignable to the destination type, no conversion is necessary.
|
|
|
|
if to.AssignableFrom(x.Type()) && sameSchemaTypes(to, x.Type()) {
|
|
|
|
return x, typeChanged
|
2020-05-29 10:38:53 +00:00
|
|
|
}
|
2020-06-02 19:00:35 +00:00
|
|
|
|
|
|
|
// Otherwise, wrap the expression in a call to __convert.
|
|
|
|
return NewConvertCall(x, to), true
|
|
|
|
}
|
|
|
|
|
2021-04-16 12:04:42 +00:00
|
|
|
// resolveDiscriminatedUnions reduces discriminated unions of object types to the type that matches
|
|
|
|
// the shape of the given object cons expression. A given object expression would only match a single
|
|
|
|
// case of the union.
|
|
|
|
func resolveDiscriminatedUnions(obj *model.ObjectConsExpression, modelType model.Type) model.Type {
|
|
|
|
modelUnion, ok := modelType.(*model.UnionType)
|
|
|
|
if !ok {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
schType, ok := GetSchemaForType(modelUnion)
|
|
|
|
if !ok {
|
|
|
|
return nil
|
|
|
|
}
|
2021-06-24 16:17:55 +00:00
|
|
|
schType = codegen.UnwrapType(schType)
|
2021-04-16 12:04:42 +00:00
|
|
|
union, ok := schType.(*schema.UnionType)
|
|
|
|
if !ok || union.Discriminator == "" {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
objTypes := GetDiscriminatedUnionObjectMapping(modelUnion)
|
|
|
|
for _, item := range obj.Items {
|
|
|
|
name, ok := item.Key.(*model.LiteralValueExpression)
|
|
|
|
if !ok || name.Value.AsString() != union.Discriminator {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2022-09-14 04:18:07 +00:00
|
|
|
// The discriminator should be a string, but it could be in the
|
|
|
|
// form of a *string wrapped in a __convert call so we try both.
|
|
|
|
var lit *model.TemplateExpression
|
|
|
|
lit, ok = item.Value.(*model.TemplateExpression)
|
|
|
|
if !ok {
|
|
|
|
var call *model.FunctionCallExpression
|
|
|
|
call, ok = item.Value.(*model.FunctionCallExpression)
|
|
|
|
if ok && call.Name == IntrinsicConvert {
|
|
|
|
lit, ok = call.Args[0].(*model.TemplateExpression)
|
|
|
|
}
|
|
|
|
}
|
2021-04-16 12:04:42 +00:00
|
|
|
if !ok {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2022-09-14 04:18:07 +00:00
|
|
|
discriminatorValue, ok := extractStringValue(lit)
|
|
|
|
if !ok {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2021-04-16 12:04:42 +00:00
|
|
|
if ref, ok := union.Mapping[discriminatorValue]; ok {
|
|
|
|
discriminatorValue = strings.TrimPrefix(ref, "#/types/")
|
|
|
|
}
|
|
|
|
if t, ok := objTypes[discriminatorValue]; ok {
|
|
|
|
return t
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-06-02 19:00:35 +00:00
|
|
|
// RewriteConversions wraps automatic conversions indicated by the HCL2 spec and conversions to schema-annotated types
|
|
|
|
// in calls to the __convert intrinsic.
|
|
|
|
//
|
|
|
|
// Note that the result is a bit out of line with the HCL2 spec, as static conversions may happen earlier than they
|
|
|
|
// would at runtime. For example, consider the case of a tuple of strings that is being converted to a list of numbers:
|
|
|
|
//
|
2022-09-14 02:12:02 +00:00
|
|
|
// [a, b, c]
|
2020-06-02 19:00:35 +00:00
|
|
|
//
|
|
|
|
// Calling RewriteConversions on this expression with a destination type of list(number) would result in this IR:
|
|
|
|
//
|
2022-09-14 02:12:02 +00:00
|
|
|
// [__convert(a), __convert(b), __convert(c)]
|
2020-06-02 19:00:35 +00:00
|
|
|
//
|
|
|
|
// If any of these conversions fail, the evaluation of the tuple itself fails. The HCL2 evaluation semantics, however,
|
|
|
|
// would convert the tuple _after_ it has been evaluated. The IR that matches these semantics is
|
|
|
|
//
|
2022-09-14 02:12:02 +00:00
|
|
|
// __convert([a, b, c])
|
2020-06-02 19:00:35 +00:00
|
|
|
//
|
|
|
|
// This transform uses the former representation so that it can appropriately insert calls to `__convert` in the face
|
|
|
|
// of schema-annotated types. There is a reasonable argument to be made that RewriteConversions should not be
|
|
|
|
// responsible for propagating schema annotations, and that this pass should be split in two: one pass would insert
|
|
|
|
// conversions that match HCL2 evaluation semantics, and another would insert calls to some separate intrinsic in order
|
|
|
|
// to propagate schema information.
|
2022-07-07 20:15:47 +00:00
|
|
|
func RewriteConversions(x model.Expression, to model.Type) (model.Expression, hcl.Diagnostics) {
|
|
|
|
var diags hcl.Diagnostics
|
|
|
|
x, _ = rewriteConversions(x, to, &diags)
|
|
|
|
return x, diags
|
2020-05-11 18:17:36 +00:00
|
|
|
}
|
2020-05-29 10:38:53 +00:00
|
|
|
|
|
|
|
// convertPrimitiveValues returns a new expression if the given expression can be converted to another primitive type
|
|
|
|
// (bool, int, number, string) that matches the target type.
|
|
|
|
func convertPrimitiveValues(from model.Expression, to model.Type) (model.Expression, bool) {
|
|
|
|
var expression model.Expression
|
|
|
|
switch {
|
2023-05-02 14:07:58 +00:00
|
|
|
case from == nil || to == nil:
|
|
|
|
return from, false
|
2020-06-02 19:00:35 +00:00
|
|
|
case to.AssignableFrom(from.Type()) || to.AssignableFrom(model.DynamicType):
|
2020-05-29 10:38:53 +00:00
|
|
|
return nil, false
|
|
|
|
case to.AssignableFrom(model.BoolType):
|
|
|
|
if stringLiteral, ok := extractStringValue(from); ok {
|
|
|
|
if value, err := convert.Convert(cty.StringVal(stringLiteral), cty.Bool); err == nil {
|
|
|
|
expression = &model.LiteralValueExpression{Value: value}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
case to.AssignableFrom(model.IntType), to.AssignableFrom(model.NumberType):
|
|
|
|
if stringLiteral, ok := extractStringValue(from); ok {
|
|
|
|
if value, err := convert.Convert(cty.StringVal(stringLiteral), cty.Number); err == nil {
|
|
|
|
expression = &model.LiteralValueExpression{Value: value}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
case to.AssignableFrom(model.StringType):
|
|
|
|
if stringValue, ok := convertLiteralToString(from); ok {
|
|
|
|
expression = &model.TemplateExpression{
|
|
|
|
Parts: []model.Expression{&model.LiteralValueExpression{
|
|
|
|
Value: cty.StringVal(stringValue),
|
|
|
|
}},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if expression == nil {
|
|
|
|
return nil, false
|
|
|
|
}
|
|
|
|
|
2020-06-02 19:00:35 +00:00
|
|
|
diags := expression.Typecheck(false)
|
2023-02-17 01:23:09 +00:00
|
|
|
contract.Assertf(len(diags) == 0, "error typechecking expression: %v", diags)
|
2020-06-02 19:00:35 +00:00
|
|
|
|
2020-05-29 10:38:53 +00:00
|
|
|
expression.SetLeadingTrivia(from.GetLeadingTrivia())
|
|
|
|
expression.SetTrailingTrivia(from.GetTrailingTrivia())
|
|
|
|
return expression, true
|
|
|
|
}
|
|
|
|
|
|
|
|
// extractStringValue returns a string if the given expression is a template expression containing a single string
|
|
|
|
// literal value.
|
|
|
|
func extractStringValue(arg model.Expression) (string, bool) {
|
|
|
|
template, ok := arg.(*model.TemplateExpression)
|
|
|
|
if !ok || len(template.Parts) != 1 {
|
|
|
|
return "", false
|
|
|
|
}
|
|
|
|
lit, ok := template.Parts[0].(*model.LiteralValueExpression)
|
2021-06-24 16:17:55 +00:00
|
|
|
if !ok || model.StringType.ConversionFrom(lit.Type()) == model.NoConversion {
|
2020-05-29 10:38:53 +00:00
|
|
|
return "", false
|
|
|
|
}
|
|
|
|
return lit.Value.AsString(), true
|
|
|
|
}
|
|
|
|
|
|
|
|
// convertLiteralToString converts a literal of type Bool, Int, or Number to its string representation. It also handles
|
|
|
|
// the unary negate operation in front of a literal number.
|
|
|
|
func convertLiteralToString(from model.Expression) (string, bool) {
|
|
|
|
switch expr := from.(type) {
|
|
|
|
case *model.UnaryOpExpression:
|
|
|
|
if expr.Operation == hclsyntax.OpNegate {
|
|
|
|
if operandValue, ok := convertLiteralToString(expr.Operand); ok {
|
|
|
|
return "-" + operandValue, true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
case *model.LiteralValueExpression:
|
|
|
|
if stringValue, err := convert.Convert(expr.Value, cty.String); err == nil {
|
2023-06-09 10:52:10 +00:00
|
|
|
if stringValue.IsNull() {
|
|
|
|
return "", false
|
|
|
|
}
|
2020-05-29 10:38:53 +00:00
|
|
|
return stringValue.AsString(), true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return "", false
|
|
|
|
}
|
2022-04-18 09:03:42 +00:00
|
|
|
|
[program-gen] Fix enum resolution from types of the form Union[string, Enum] and emit fully qualified enum cases (#15696)
# Description
This PR improves enum type resolution from strings. When we try to
resolve `Union[string, Enum]` for a string expression, we choose
`string` because it is the more general type since not every string is
assignable to `Enum`. However, here we spacial case strings that are
actually part of that `Enum`.
The result is that `pcl.LowerConversion` will choose `Enum` from
`Union[string, Enum]` when the value of the input string is compatible
with the enum. This greatly improves program-gen for all of typescript,
python, csharp and go which now will emit the fully qualified enum cases
instead of emitting strings.
Closes https://github.com/pulumi/pulumi-dotnet/issues/41 which is
supposed to be a duplicate of
https://github.com/pulumi/pulumi-azure-native/issues/2616 but that is
not the case (the former is about unions of objects, the latter is
unions of enums and strings)
## Checklist
- [ ] I have run `make tidy` to update any new dependencies
- [x] I have run `make lint` to verify my code passes the lint check
- [x] I have formatted my code using `gofumpt`
<!--- Please provide details if the checkbox below is to be left
unchecked. -->
- [x] I have added tests that prove my fix is effective or that my
feature works
<!---
User-facing changes require a CHANGELOG entry.
-->
- [x] I have run `make changelog` and committed the
`changelog/pending/<file>` documenting my change
<!--
If the change(s) in this PR is a modification of an existing call to the
Pulumi Cloud,
then the service should honor older versions of the CLI where this
change would not exist.
You must then bump the API version in
/pkg/backend/httpstate/client/api.go, as well as add
it to the service.
-->
- [ ] Yes, there are changes in this PR that warrants bumping the Pulumi
Cloud API version
<!-- @Pulumi employees: If yes, you must submit corresponding changes in
the service repo. -->
2024-03-15 17:49:12 +00:00
|
|
|
func literalExprValue(expr model.Expression) (cty.Value, bool) {
|
|
|
|
if lit, ok := expr.(*model.LiteralValueExpression); ok {
|
|
|
|
return lit.Value, true
|
|
|
|
}
|
|
|
|
|
|
|
|
if templateExpr, ok := expr.(*model.TemplateExpression); ok {
|
|
|
|
if len(templateExpr.Parts) == 1 {
|
|
|
|
return literalExprValue(templateExpr.Parts[0])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return cty.NilVal, false
|
|
|
|
}
|
|
|
|
|
2022-04-20 07:47:34 +00:00
|
|
|
// lowerConversion performs the main logic of LowerConversion. nil, false is
|
|
|
|
// returned if there is no conversion (safe or unsafe) between `from` and `to`.
|
|
|
|
// This can occur when a loosely typed program is converted, or if an other
|
|
|
|
// rewrite violated the type system.
|
|
|
|
func lowerConversion(from model.Expression, to model.Type) (model.Type, bool) {
|
|
|
|
switch to := to.(type) {
|
|
|
|
case *model.UnionType:
|
|
|
|
// Assignment: it just works
|
|
|
|
for _, to := range to.ElementTypes {
|
[program-gen] Fix enum resolution from types of the form Union[string, Enum] and emit fully qualified enum cases (#15696)
# Description
This PR improves enum type resolution from strings. When we try to
resolve `Union[string, Enum]` for a string expression, we choose
`string` because it is the more general type since not every string is
assignable to `Enum`. However, here we spacial case strings that are
actually part of that `Enum`.
The result is that `pcl.LowerConversion` will choose `Enum` from
`Union[string, Enum]` when the value of the input string is compatible
with the enum. This greatly improves program-gen for all of typescript,
python, csharp and go which now will emit the fully qualified enum cases
instead of emitting strings.
Closes https://github.com/pulumi/pulumi-dotnet/issues/41 which is
supposed to be a duplicate of
https://github.com/pulumi/pulumi-azure-native/issues/2616 but that is
not the case (the former is about unions of objects, the latter is
unions of enums and strings)
## Checklist
- [ ] I have run `make tidy` to update any new dependencies
- [x] I have run `make lint` to verify my code passes the lint check
- [x] I have formatted my code using `gofumpt`
<!--- Please provide details if the checkbox below is to be left
unchecked. -->
- [x] I have added tests that prove my fix is effective or that my
feature works
<!---
User-facing changes require a CHANGELOG entry.
-->
- [x] I have run `make changelog` and committed the
`changelog/pending/<file>` documenting my change
<!--
If the change(s) in this PR is a modification of an existing call to the
Pulumi Cloud,
then the service should honor older versions of the CLI where this
change would not exist.
You must then bump the API version in
/pkg/backend/httpstate/client/api.go, as well as add
it to the service.
-->
- [ ] Yes, there are changes in this PR that warrants bumping the Pulumi
Cloud API version
<!-- @Pulumi employees: If yes, you must submit corresponding changes in
the service repo. -->
2024-03-15 17:49:12 +00:00
|
|
|
// in general, strings are not assignable to enums, but we allow it here
|
|
|
|
// if the enum has an element that matches the `from` expression
|
|
|
|
switch enumType := to.(type) {
|
|
|
|
case *model.EnumType:
|
|
|
|
if literal, ok := literalExprValue(from); ok {
|
|
|
|
for _, enumCase := range enumType.Elements {
|
|
|
|
if enumCase.RawEquals(literal) {
|
|
|
|
return to, true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-20 07:47:34 +00:00
|
|
|
if to.AssignableFrom(from.Type()) {
|
|
|
|
return to, true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
conversions := make([]model.ConversionKind, len(to.ElementTypes))
|
|
|
|
for i, to := range to.ElementTypes {
|
|
|
|
conversions[i] = to.ConversionFrom(from.Type())
|
|
|
|
if conversions[i] == model.SafeConversion {
|
|
|
|
// We found a safe conversion, and we will use it. We don't need
|
|
|
|
// to search for more conversions.
|
|
|
|
return to, true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Unsafe conversions:
|
|
|
|
for i, to := range to.ElementTypes {
|
|
|
|
if conversions[i] == model.UnsafeConversion {
|
|
|
|
return to, true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil, false
|
|
|
|
default:
|
|
|
|
return to, true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-18 09:03:42 +00:00
|
|
|
// LowerConversion lowers a conversion for a specific value, such that
|
|
|
|
// converting `from` to a value of the returned type will produce valid code.
|
2022-04-20 07:47:34 +00:00
|
|
|
// The algorithm prioritizes safe conversions over unsafe conversions. If no
|
|
|
|
// conversion can be found, nil, false is returned.
|
2022-04-18 09:03:42 +00:00
|
|
|
//
|
|
|
|
// This is useful because it cuts out conversion steps which the caller doesn't
|
|
|
|
// need to worry about. For example:
|
|
|
|
// Given inputs
|
|
|
|
//
|
2022-09-14 02:12:02 +00:00
|
|
|
// from = string("foo") # a constant string with value "foo"
|
|
|
|
// to = union(enum(string: "foo", "bar"), input(enum(string: "foo", "bar")), none)
|
2022-04-18 09:03:42 +00:00
|
|
|
//
|
|
|
|
// We would receive output type:
|
|
|
|
//
|
2022-09-14 02:12:02 +00:00
|
|
|
// enum(string: "foo", "bar")
|
2022-04-18 09:03:42 +00:00
|
|
|
//
|
|
|
|
// since the caller can convert string("foo") to the enum directly, and does not
|
|
|
|
// need to consider the union.
|
|
|
|
//
|
|
|
|
// For another example consider inputs:
|
|
|
|
//
|
2022-09-14 02:12:02 +00:00
|
|
|
// from = var(string) # A variable of type string
|
|
|
|
// to = union(enum(string: "foo", "bar"), string)
|
2022-04-18 09:03:42 +00:00
|
|
|
//
|
|
|
|
// We would return type:
|
|
|
|
//
|
2022-09-14 02:12:02 +00:00
|
|
|
// string
|
2022-04-18 09:03:42 +00:00
|
|
|
//
|
|
|
|
// since var(string) can be safely assigned to string, but unsafely assigned to
|
|
|
|
// enum(string: "foo", "bar").
|
|
|
|
func LowerConversion(from model.Expression, to model.Type) model.Type {
|
2022-04-20 07:47:34 +00:00
|
|
|
if t, ok := lowerConversion(from, to); ok {
|
|
|
|
return t
|
2022-04-18 09:03:42 +00:00
|
|
|
}
|
2022-04-20 07:47:34 +00:00
|
|
|
return to
|
2022-04-18 09:03:42 +00:00
|
|
|
}
|