github.com/myhau/pulumi/pkg/v3@v3.70.2-0.20221116134521-f2775972e587/codegen/nodejs/gen_program_lower.go (about)

     1  package nodejs
     2  
     3  import (
     4  	"github.com/hashicorp/hcl/v2"
     5  	"github.com/pulumi/pulumi/pkg/v3/codegen"
     6  	"github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/model"
     7  	"github.com/pulumi/pulumi/pkg/v3/codegen/pcl"
     8  	"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
     9  )
    10  
    11  func isOutputType(t model.Type) bool {
    12  	switch t := t.(type) {
    13  	case *model.OutputType:
    14  		return true
    15  	case *model.UnionType:
    16  		for _, t := range t.ElementTypes {
    17  			if _, isOutput := t.(*model.OutputType); isOutput {
    18  				return true
    19  			}
    20  		}
    21  	}
    22  	return false
    23  }
    24  
    25  func isPromiseType(t model.Type) bool {
    26  	switch t := t.(type) {
    27  	case *model.PromiseType:
    28  		return true
    29  	case *model.UnionType:
    30  		isPromise := false
    31  		for _, t := range t.ElementTypes {
    32  			switch t.(type) {
    33  			case *model.OutputType:
    34  				return false
    35  			case *model.PromiseType:
    36  				isPromise = true
    37  			}
    38  		}
    39  		return isPromise
    40  	}
    41  	return false
    42  }
    43  
    44  func isParameterReference(parameters codegen.Set, x model.Expression) bool {
    45  	scopeTraversal, ok := x.(*model.ScopeTraversalExpression)
    46  	if !ok {
    47  		return false
    48  	}
    49  
    50  	return parameters.Has(scopeTraversal.Parts[0])
    51  }
    52  
    53  // canLiftTraversal returns true if this traversal can be lifted. Any traversal that does not traverse
    54  // possibly-undefined values can be lifted.
    55  func (g *generator) canLiftTraversal(parts []model.Traversable) bool {
    56  	for _, p := range parts {
    57  		t := model.GetTraversableType(p)
    58  		if model.IsOptionalType(t) || isPromiseType(t) {
    59  			return false
    60  		}
    61  	}
    62  	return true
    63  }
    64  
    65  // parseProxyApply attempts to match and rewrite the given parsed apply using the following patterns:
    66  //
    67  // - __apply(<expr>, eval(x, x[index])) -> <expr>[index]
    68  // - __apply(<expr>, eval(x, x.attr))) -> <expr>.attr
    69  // - __apply(scope.traversal, eval(x, x.attr)) -> scope.traversal.attr
    70  //
    71  // Each of these patterns matches an apply that can be handled by `pulumi.Output`'s property access proxy.
    72  func (g *generator) parseProxyApply(parameters codegen.Set, args []model.Expression,
    73  	then model.Expression) (model.Expression, bool) {
    74  
    75  	if len(args) != 1 {
    76  		return nil, false
    77  	}
    78  
    79  	arg := args[0]
    80  	switch then := then.(type) {
    81  	case *model.IndexExpression:
    82  		t := arg.Type()
    83  		if !isParameterReference(parameters, then.Collection) || model.IsOptionalType(t) || isPromiseType(t) {
    84  			return nil, false
    85  		}
    86  		then.Collection = arg
    87  	case *model.ScopeTraversalExpression:
    88  		if !isParameterReference(parameters, then) || isPromiseType(arg.Type()) {
    89  			return nil, false
    90  		}
    91  		if !g.canLiftTraversal(then.Parts) {
    92  			return nil, false
    93  		}
    94  
    95  		switch arg := arg.(type) {
    96  		case *model.RelativeTraversalExpression:
    97  			arg.Traversal = append(arg.Traversal, then.Traversal[1:]...)
    98  			arg.Parts = append(arg.Parts, then.Parts...)
    99  		case *model.ScopeTraversalExpression:
   100  			arg.Traversal = append(arg.Traversal, then.Traversal[1:]...)
   101  			arg.Parts = append(arg.Parts, then.Parts...)
   102  		default:
   103  			return nil, false
   104  		}
   105  	default:
   106  		return nil, false
   107  	}
   108  
   109  	diags := arg.Typecheck(false)
   110  	contract.Assert(len(diags) == 0)
   111  	return arg, true
   112  }
   113  
   114  func callbackParameterReferences(expr model.Expression, parameters codegen.Set) []*model.Variable {
   115  	var refs []*model.Variable
   116  	visitor := func(expr model.Expression) (model.Expression, hcl.Diagnostics) {
   117  		if expr, isScopeTraversal := expr.(*model.ScopeTraversalExpression); isScopeTraversal {
   118  			if parameters.Has(expr.Parts[0]) {
   119  				refs = append(refs, expr.Parts[0].(*model.Variable))
   120  			}
   121  		}
   122  		return expr, nil
   123  	}
   124  
   125  	_, diags := model.VisitExpression(expr, model.IdentityVisitor, visitor)
   126  	contract.Assert(len(diags) == 0)
   127  	return refs
   128  }
   129  
   130  // parseInterpolate attempts to match the given parsed apply against a template whose parts are a mix of prompt
   131  // expressions and proxyable applies.
   132  //
   133  // If a match is found, parseInterpolate returns an appropriate call to the __interpolate intrinsic with a mix of
   134  // expressions and proxied applies.
   135  func (g *generator) parseInterpolate(parameters codegen.Set, args []model.Expression,
   136  	then *model.AnonymousFunctionExpression) (model.Expression, bool) {
   137  
   138  	template, ok := then.Body.(*model.TemplateExpression)
   139  	if !ok {
   140  		return nil, false
   141  	}
   142  
   143  	indices := map[*model.Variable]int{}
   144  	for i, p := range then.Parameters {
   145  		indices[p] = i
   146  	}
   147  
   148  	exprs := make([]model.Expression, len(template.Parts))
   149  	for i, expr := range template.Parts {
   150  		parameterRefs := callbackParameterReferences(expr, parameters)
   151  		if len(parameterRefs) == 0 {
   152  			exprs[i] = expr
   153  			continue
   154  		}
   155  
   156  		proxyArgs := make([]model.Expression, len(parameterRefs))
   157  		for i, p := range parameterRefs {
   158  			argIndex, ok := indices[p]
   159  			contract.Assert(ok)
   160  
   161  			proxyArgs[i] = args[argIndex]
   162  		}
   163  
   164  		expr, ok := g.parseProxyApply(parameters, proxyArgs, expr)
   165  		if !ok {
   166  			return nil, false
   167  		}
   168  		exprs[i] = expr
   169  	}
   170  
   171  	return newInterpolateCall(exprs), true
   172  }
   173  
   174  // lowerProxyApplies lowers certain calls to the apply intrinsic into proxied property accesses and/or calls to the
   175  // pulumi.interpolate function. Concretely, this boils down to rewriting the following shapes
   176  //
   177  // - __apply(<expr>, eval(x, x[index]))
   178  // - __apply(<expr>, eval(x, x.attr))
   179  // - __apply(scope.traversal, eval(x, x.attr))
   180  // - __apply(<proxy-apply>, ..., eval(x, ..., "foo ${<proxy-apply>} bar ${...}"))
   181  //
   182  // into (respectively)
   183  //
   184  // - <expr>[index]
   185  // - <expr>.attr
   186  // - scope.traversal.attr
   187  // - __interpolate("foo ", <proxy-apply>, " bar ", ...)
   188  //
   189  // The first two forms will be generated as proxied applies; the lattermost will be generated as an interpolated string
   190  // that uses `pulumi.interpolate`.
   191  func (g *generator) lowerProxyApplies(expr model.Expression) (model.Expression, hcl.Diagnostics) {
   192  	rewriter := func(expr model.Expression) (model.Expression, hcl.Diagnostics) {
   193  		// Ignore the node if it is not a call to the apply intrinsic.
   194  		apply, ok := expr.(*model.FunctionCallExpression)
   195  		if !ok || apply.Name != pcl.IntrinsicApply {
   196  			return expr, nil
   197  		}
   198  
   199  		// Parse the apply call.
   200  		args, then := pcl.ParseApplyCall(apply)
   201  
   202  		parameters := codegen.Set{}
   203  		for _, p := range then.Parameters {
   204  			parameters.Add(p)
   205  		}
   206  
   207  		// Attempt to match (call __apply (rvar) (call __applyArg 0))
   208  		if v, ok := g.parseProxyApply(parameters, args, then.Body); ok {
   209  			return v, nil
   210  		}
   211  
   212  		// Attempt to match (call __apply (rvar 0) ... (rvar n) (output /* mix of literals and calls to __applyArg)
   213  		if v, ok := g.parseInterpolate(parameters, args, then); ok {
   214  			return v, nil
   215  		}
   216  
   217  		return expr, nil
   218  	}
   219  	return model.VisitExpression(expr, model.IdentityVisitor, rewriter)
   220  }
   221  
   222  // awaitInvokes wraps each call to `invoke` with a call to the `await` intrinsic. This rewrite should only be used
   223  // if we are generating an async main, in which case the apply rewriter should also be configured not to treat
   224  // promises as eventuals. The cumulative effect of these options is to avoid the use of `then` within async contexts.
   225  // Note that this depends on the fact that invokes are the only way to introduce promises in to a Pulumi program; if
   226  // this changes in the future, this transform will need to be applied in a more general way (e.g. by the apply
   227  // rewriter).
   228  func (g *generator) awaitInvokes(x model.Expression) model.Expression {
   229  	contract.Assert(g.asyncMain)
   230  
   231  	rewriter := func(x model.Expression) (model.Expression, hcl.Diagnostics) {
   232  		// Ignore the node if it is not a call to invoke.
   233  		call, ok := x.(*model.FunctionCallExpression)
   234  		if !ok || call.Name != pcl.Invoke {
   235  			return x, nil
   236  		}
   237  
   238  		_, isPromise := call.Type().(*model.PromiseType)
   239  		contract.Assert(isPromise)
   240  
   241  		return newAwaitCall(call), nil
   242  	}
   243  	x, diags := model.VisitExpression(x, model.IdentityVisitor, rewriter)
   244  	contract.Assert(len(diags) == 0)
   245  	return x
   246  }