/
rewrite_convert.go
423 lines (387 loc) 路 13.8 KB
/
rewrite_convert.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
package pcl
import (
"strings"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"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"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/convert"
)
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
}
// rewriteConversions implements the core of RewriteConversions. It returns the rewritten expression and true if the
// type of the expression may have changed.
func rewriteConversions(x model.Expression, to model.Type, diags *hcl.Diagnostics) (model.Expression, bool) {
if x == nil || to == nil {
return x, false
}
// 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
switch x := x.(type) {
case *model.AnonymousFunctionExpression:
x.Body, _ = rewriteConversions(x.Body, to, diags)
case *model.BinaryOpExpression:
x.LeftOperand, _ = rewriteConversions(x.LeftOperand, model.InputType(x.LeftOperandType()), diags)
x.RightOperand, _ = rewriteConversions(x.RightOperand, model.InputType(x.RightOperandType()), diags)
case *model.ConditionalExpression:
var trueChanged, falseChanged bool
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)
typecheck = trueChanged || falseChanged
case *model.ForExpression:
traverserType := model.NumberType
if x.Key != nil {
traverserType = model.StringType
x.Key, _ = rewriteConversions(x.Key, model.InputType(model.StringType), diags)
}
if x.Condition != nil {
x.Condition, _ = rewriteConversions(x.Condition, model.InputType(model.BoolType), diags)
}
valueType, tdiags := to.Traverse(model.MakeTraverser(traverserType))
*diags = diags.Extend(tdiags)
x.Value, typecheck = rewriteConversions(x.Value, valueType.(model.Type), diags)
case *model.FunctionCallExpression:
args := x.Args
for _, param := range x.Signature.Parameters {
if len(args) == 0 {
break
}
args[0], _ = rewriteConversions(args[0], model.InputType(param.Type), diags)
args = args[1:]
}
if x.Signature.VarargsParameter != nil {
for i := range args {
args[i], _ = rewriteConversions(args[i], model.InputType(x.Signature.VarargsParameter.Type), diags)
}
}
case *model.IndexExpression:
x.Key, _ = rewriteConversions(x.Key, x.KeyType(), diags)
case *model.ObjectConsExpression:
if v := resolveDiscriminatedUnions(x, to); v != nil {
to = v
typecheck = true
}
for i := range x.Items {
item := &x.Items[i]
if item.Key.Type() == model.DynamicType {
// We don't know the type of this expression, so we can't correct the
// type.
continue
}
key, ediags := item.Key.Evaluate(&hcl.EvalContext{}) // empty context, we need a constant string
*diags = diags.Extend(ediags)
valueType, tdiags := to.Traverse(hcl.TraverseIndex{
Key: key,
SrcRange: item.Key.SyntaxNode().Range(),
})
*diags = diags.Extend(tdiags)
var valueChanged bool
item.Key, _ = rewriteConversions(item.Key, model.InputType(model.StringType), diags)
item.Value, valueChanged = rewriteConversions(item.Value, valueType.(model.Type), diags)
typecheck = typecheck || valueChanged
}
case *model.TupleConsExpression:
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(),
})
*diags = diags.Extend(tdiags)
var exprChanged bool
x.Expressions[i], exprChanged = rewriteConversions(expr, valueType.(model.Type), diags)
typecheck = typecheck || exprChanged
}
case *model.UnaryOpExpression:
x.Operand, _ = rewriteConversions(x.Operand, model.InputType(x.OperandType()), diags)
}
var typeChanged bool
if typecheck {
typecheckDiags := x.Typecheck(false)
*diags = diags.Extend(typecheckDiags)
typeChanged = true
}
// If we can convert a primitive value in place, do so.
if value, ok := convertPrimitiveValues(x, to); ok {
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
}
// Otherwise, wrap the expression in a call to __convert.
return NewConvertCall(x, to), true
}
// 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
}
schType = codegen.UnwrapType(schType)
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
}
// 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)
}
}
if !ok {
continue
}
discriminatorValue, ok := extractStringValue(lit)
if !ok {
return nil
}
if ref, ok := union.Mapping[discriminatorValue]; ok {
discriminatorValue = strings.TrimPrefix(ref, "#/types/")
}
if t, ok := objTypes[discriminatorValue]; ok {
return t
}
}
return nil
}
// 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:
//
// [a, b, c]
//
// Calling RewriteConversions on this expression with a destination type of list(number) would result in this IR:
//
// [__convert(a), __convert(b), __convert(c)]
//
// 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
//
// __convert([a, b, c])
//
// 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.
func RewriteConversions(x model.Expression, to model.Type) (model.Expression, hcl.Diagnostics) {
var diags hcl.Diagnostics
x, _ = rewriteConversions(x, to, &diags)
return x, diags
}
// 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 {
case from == nil || to == nil:
return from, false
case to.AssignableFrom(from.Type()) || to.AssignableFrom(model.DynamicType):
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
}
diags := expression.Typecheck(false)
contract.Assertf(len(diags) == 0, "error typechecking expression: %v", diags)
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)
if !ok || model.StringType.ConversionFrom(lit.Type()) == model.NoConversion {
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 {
if stringValue.IsNull() {
return "", false
}
return stringValue.AsString(), true
}
}
return "", false
}
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
}
// 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 {
// 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
}
}
}
}
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
}
}
// LowerConversion lowers a conversion for a specific value, such that
// converting `from` to a value of the returned type will produce valid code.
// The algorithm prioritizes safe conversions over unsafe conversions. If no
// conversion can be found, nil, false is returned.
//
// This is useful because it cuts out conversion steps which the caller doesn't
// need to worry about. For example:
// Given inputs
//
// from = string("foo") # a constant string with value "foo"
// to = union(enum(string: "foo", "bar"), input(enum(string: "foo", "bar")), none)
//
// We would receive output type:
//
// enum(string: "foo", "bar")
//
// since the caller can convert string("foo") to the enum directly, and does not
// need to consider the union.
//
// For another example consider inputs:
//
// from = var(string) # A variable of type string
// to = union(enum(string: "foo", "bar"), string)
//
// We would return type:
//
// string
//
// 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 {
if t, ok := lowerConversion(from, to); ok {
return t
}
return to
}