Some of the apply rewriter's assumptions were broken by the richer expressions available in HCL2. These changes fix those broken assumptions, in particular the assumption that only scope traversal expressions are sources of eventual values.
551 lines
16 KiB
Go
551 lines
16 KiB
Go
package nodejs
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"math/big"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/hcl/v2"
|
|
"github.com/hashicorp/hcl/v2/hclsyntax"
|
|
"github.com/pulumi/pulumi/pkg/v2/codegen/hcl2"
|
|
"github.com/pulumi/pulumi/pkg/v2/codegen/hcl2/model"
|
|
"github.com/pulumi/pulumi/sdk/v2/go/common/util/contract"
|
|
"github.com/zclconf/go-cty/cty"
|
|
"github.com/zclconf/go-cty/cty/convert"
|
|
)
|
|
|
|
type nameInfo int
|
|
|
|
func (nameInfo) Format(name string) string {
|
|
return cleanName(name)
|
|
}
|
|
|
|
func (g *generator) lowerExpression(expr model.Expression) model.Expression {
|
|
// TODO(pdg): diagnostics
|
|
expr, _ = hcl2.RewriteApplies(expr, nameInfo(0), true)
|
|
expr, _ = g.lowerProxyApplies(expr)
|
|
return expr
|
|
}
|
|
|
|
func (g *generator) GetPrecedence(expr model.Expression) int {
|
|
// Precedence is derived from
|
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence.
|
|
switch expr := expr.(type) {
|
|
case *model.ConditionalExpression:
|
|
return 4
|
|
case *model.BinaryOpExpression:
|
|
switch expr.Operation {
|
|
case hclsyntax.OpLogicalOr:
|
|
return 5
|
|
case hclsyntax.OpLogicalAnd:
|
|
return 6
|
|
case hclsyntax.OpEqual, hclsyntax.OpNotEqual:
|
|
return 11
|
|
case hclsyntax.OpGreaterThan, hclsyntax.OpGreaterThanOrEqual, hclsyntax.OpLessThan,
|
|
hclsyntax.OpLessThanOrEqual:
|
|
return 12
|
|
case hclsyntax.OpAdd, hclsyntax.OpSubtract:
|
|
return 14
|
|
case hclsyntax.OpMultiply, hclsyntax.OpDivide, hclsyntax.OpModulo:
|
|
return 15
|
|
default:
|
|
contract.Failf("unexpected binary expression %v", expr)
|
|
}
|
|
case *model.UnaryOpExpression:
|
|
return 17
|
|
case *model.FunctionCallExpression:
|
|
switch expr.Name {
|
|
case intrinsicAwait:
|
|
return 17
|
|
case intrinsicInterpolate:
|
|
return 22
|
|
default:
|
|
return 20
|
|
}
|
|
case *model.ForExpression, *model.IndexExpression, *model.RelativeTraversalExpression, *model.SplatExpression,
|
|
*model.TemplateJoinExpression:
|
|
return 20
|
|
case *model.AnonymousFunctionExpression, *model.LiteralValueExpression, *model.ObjectConsExpression,
|
|
*model.ScopeTraversalExpression, *model.TemplateExpression, *model.TupleConsExpression:
|
|
return 22
|
|
default:
|
|
contract.Failf("unexpected expression %v of type %T", expr, expr)
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func (g *generator) GenAnonymousFunctionExpression(w io.Writer, expr *model.AnonymousFunctionExpression) {
|
|
switch len(expr.Signature.Parameters) {
|
|
case 0:
|
|
g.Fgen(w, "()")
|
|
case 1:
|
|
g.Fgenf(w, "%s", expr.Signature.Parameters[0].Name)
|
|
default:
|
|
g.Fgen(w, "([")
|
|
for i, p := range expr.Signature.Parameters {
|
|
if i > 0 {
|
|
g.Fgen(w, ", ")
|
|
}
|
|
g.Fgenf(w, "%s", p.Name)
|
|
}
|
|
g.Fgen(w, "])")
|
|
}
|
|
|
|
g.Fgenf(w, " => %.v", expr.Body)
|
|
}
|
|
|
|
func (g *generator) GenBinaryOpExpression(w io.Writer, expr *model.BinaryOpExpression) {
|
|
opstr, precedence := "", g.GetPrecedence(expr)
|
|
switch expr.Operation {
|
|
case hclsyntax.OpAdd:
|
|
opstr = "+"
|
|
case hclsyntax.OpDivide:
|
|
opstr = "/"
|
|
case hclsyntax.OpEqual:
|
|
opstr = "=="
|
|
case hclsyntax.OpGreaterThan:
|
|
opstr = ">"
|
|
case hclsyntax.OpGreaterThanOrEqual:
|
|
opstr = ">="
|
|
case hclsyntax.OpLessThan:
|
|
opstr = "<"
|
|
case hclsyntax.OpLessThanOrEqual:
|
|
opstr = "<="
|
|
case hclsyntax.OpLogicalAnd:
|
|
opstr = "&&"
|
|
case hclsyntax.OpLogicalOr:
|
|
opstr = "||"
|
|
case hclsyntax.OpModulo:
|
|
opstr = "%"
|
|
case hclsyntax.OpMultiply:
|
|
opstr = "*"
|
|
case hclsyntax.OpNotEqual:
|
|
opstr = "!="
|
|
case hclsyntax.OpSubtract:
|
|
opstr = "-"
|
|
default:
|
|
opstr, precedence = ",", 1
|
|
}
|
|
|
|
g.Fgenf(w, "%.[1]*[2]v %[3]v %.[1]*[4]o", precedence, expr.LeftOperand, opstr, expr.RightOperand)
|
|
}
|
|
|
|
func (g *generator) GenConditionalExpression(w io.Writer, expr *model.ConditionalExpression) {
|
|
g.Fgenf(w, "%.4v ? %.4v : %.4v", expr.Condition, expr.TrueResult, expr.FalseResult)
|
|
}
|
|
|
|
func (g *generator) GenForExpression(w io.Writer, expr *model.ForExpression) {
|
|
switch expr.Collection.Type().(type) {
|
|
case *model.ListType, *model.TupleType:
|
|
if expr.KeyVariable == nil {
|
|
g.Fgenf(w, "%.20v", expr.Collection)
|
|
} else {
|
|
g.Fgenf(w, "%.20v.map((v, k) => [k, v])", expr.Collection)
|
|
}
|
|
case *model.MapType, *model.ObjectType:
|
|
if expr.KeyVariable == nil {
|
|
g.Fgenf(w, "Object.values(%.v)", expr.Collection)
|
|
} else {
|
|
g.Fgenf(w, "Object.entries(%.v)", expr.Collection)
|
|
}
|
|
}
|
|
|
|
fnParams, reduceParams := expr.ValueVariable.Name, expr.ValueVariable.Name
|
|
if expr.KeyVariable != nil {
|
|
reduceParams = fmt.Sprintf("[%.v, %.v]", expr.KeyVariable.Name, expr.ValueVariable.Name)
|
|
fnParams = fmt.Sprintf("(%v)", reduceParams)
|
|
}
|
|
|
|
if expr.Condition != nil {
|
|
g.Fgenf(w, ".filter(%s => %.v)", fnParams, expr.Condition)
|
|
}
|
|
|
|
if expr.Key != nil {
|
|
// TODO(pdg): grouping
|
|
g.Fgenf(w, ".reduce((__obj, %s) => { ...__obj, [%.v]: %.v })", reduceParams, expr.Key, expr.Value)
|
|
} else {
|
|
g.Fgenf(w, ".map(%s => %.v)", fnParams, expr.Value)
|
|
}
|
|
}
|
|
|
|
func (g *generator) genApply(w io.Writer, expr *model.FunctionCallExpression) {
|
|
// Extract the list of outputs and the continuation expression from the `__apply` arguments.
|
|
applyArgs, then := hcl2.ParseApplyCall(expr)
|
|
|
|
// If all of the arguments are promises, use promise methods. If any argument is an output, convert all other args
|
|
// to outputs and use output methods.
|
|
isOutput := make([]bool, len(applyArgs))
|
|
anyOutputs := false
|
|
for i, arg := range applyArgs {
|
|
isOutput[i] = isOutputType(arg.Type())
|
|
anyOutputs = anyOutputs || isOutput[i]
|
|
}
|
|
|
|
apply, all := "then", "Promise.all"
|
|
if anyOutputs {
|
|
apply, all = "apply", "pulumi.all"
|
|
}
|
|
|
|
if len(applyArgs) == 1 {
|
|
// If we only have a single output, just generate a normal `.apply` or `.then`.
|
|
g.Fgenf(w, "%.20v.%v(%.v)", applyArgs[0], apply, then)
|
|
} else {
|
|
// Otherwise, generate a call to `pulumi.all([]).apply()`.
|
|
g.Fgen(w, "%v([", all)
|
|
for i, o := range applyArgs {
|
|
if i > 0 {
|
|
g.Fgen(w, ", ")
|
|
}
|
|
if anyOutputs && !isOutput[i] {
|
|
g.Fgenf(w, "pulumi.output(%.v)", o)
|
|
} else {
|
|
g.Fgenf(w, "%v", o)
|
|
}
|
|
}
|
|
g.Fgenf(w, "]).%v(%.v)", apply, then)
|
|
}
|
|
}
|
|
|
|
// functionName computes the NodeJS package, module, and name for the given function token.
|
|
func functionName(tokenArg model.Expression) (string, string, string, hcl.Diagnostics) {
|
|
token := tokenArg.(*model.TemplateExpression).Parts[0].(*model.LiteralValueExpression).Value.AsString()
|
|
tokenRange := tokenArg.SyntaxNode().Range()
|
|
|
|
// Compute the resource type from the Pulumi type token.
|
|
pkg, module, member, diagnostics := hcl2.DecomposeToken(token, tokenRange)
|
|
return cleanName(pkg), strings.Replace(module, "/", ".", -1), member, diagnostics
|
|
}
|
|
|
|
func (g *generator) genRange(w io.Writer, call *model.FunctionCallExpression, entries bool) {
|
|
var from, to model.Expression
|
|
switch len(call.Args) {
|
|
case 1:
|
|
from, to = &model.LiteralValueExpression{Value: cty.NumberIntVal(0)}, call.Args[0]
|
|
case 2:
|
|
from, to = call.Args[0], call.Args[1]
|
|
default:
|
|
contract.Failf("expected range() to have exactly 1 or 2 args; got %v", len(call.Args))
|
|
}
|
|
|
|
genPrefix := func() { g.Fprint(w, "((from, to) => (new Array(to - from))") }
|
|
mapValue := "from + i"
|
|
genSuffix := func() { g.Fgenf(w, ")(%.v, %.v)", from, to) }
|
|
|
|
if litFrom, ok := from.(*model.LiteralValueExpression); ok {
|
|
fromV, err := convert.Convert(litFrom.Value, cty.Number)
|
|
contract.Assert(err == nil)
|
|
|
|
from, _ := fromV.AsBigFloat().Int64()
|
|
if litTo, ok := to.(*model.LiteralValueExpression); ok {
|
|
toV, err := convert.Convert(litTo.Value, cty.Number)
|
|
contract.Assert(err == nil)
|
|
|
|
to, _ := toV.AsBigFloat().Int64()
|
|
if from == 0 {
|
|
mapValue = "i"
|
|
} else {
|
|
mapValue = fmt.Sprintf("%d + i", from)
|
|
}
|
|
genPrefix = func() { g.Fprintf(w, "(new Array(%d))", to-from) }
|
|
genSuffix = func() {}
|
|
} else if from == 0 {
|
|
genPrefix = func() { g.Fgenf(w, "(new Array(%.v))", to) }
|
|
mapValue = "i"
|
|
genSuffix = func() {}
|
|
}
|
|
}
|
|
|
|
if entries {
|
|
mapValue = fmt.Sprintf("{key: %[1]s, value: %[1]s}", mapValue)
|
|
}
|
|
|
|
genPrefix()
|
|
g.Fprintf(w, ".map((_, i) => %v)", mapValue)
|
|
genSuffix()
|
|
}
|
|
|
|
var functionImports = map[string]string{
|
|
intrinsicInterpolate: "@pulumi/pulumi",
|
|
"fileArchive": "@pulumi/pulumi",
|
|
"fileAsset": "@pulumi/pulumi",
|
|
"readFile": "fs",
|
|
"readDir": "fs",
|
|
}
|
|
|
|
func (g *generator) getFunctionImports(x *model.FunctionCallExpression) string {
|
|
if x.Name != "invoke" {
|
|
return functionImports[x.Name]
|
|
}
|
|
|
|
pkg, _, _, diags := functionName(x.Args[0])
|
|
contract.Assert(len(diags) == 0)
|
|
return "@pulumi/" + pkg
|
|
}
|
|
|
|
func (g *generator) GenFunctionCallExpression(w io.Writer, expr *model.FunctionCallExpression) {
|
|
switch expr.Name {
|
|
case hcl2.IntrinsicApply:
|
|
g.genApply(w, expr)
|
|
case intrinsicInterpolate:
|
|
g.Fgen(w, "pulumi.interpolate`")
|
|
for _, part := range expr.Args {
|
|
if lit, ok := part.(*model.LiteralValueExpression); ok && lit.Type() == model.StringType {
|
|
g.Fgen(w, lit.Value.AsString())
|
|
} else {
|
|
g.Fgenf(w, "${%.v}", part)
|
|
}
|
|
}
|
|
g.Fgen(w, "`")
|
|
case "element":
|
|
g.Fgenf(w, "%.20v[%.v]", expr.Args[0], expr.Args[1])
|
|
case "entries":
|
|
switch expr.Args[0].Type().(type) {
|
|
case *model.ListType, *model.TupleType:
|
|
if call, ok := expr.Args[0].(*model.FunctionCallExpression); ok && call.Name == "range" {
|
|
g.genRange(w, call, true)
|
|
return
|
|
}
|
|
g.Fgenf(w, "%.20v.map((k, v)", expr.Args[0])
|
|
case *model.MapType, *model.ObjectType:
|
|
g.Fgenf(w, "Object.entries(%.v).map(([k, v])", expr.Args[0])
|
|
}
|
|
g.Fgenf(w, " => {key: k, value: v})")
|
|
case "fileArchive":
|
|
g.Fgenf(w, "new pulumi.asset.FileArchive(%.v)", expr.Args[0])
|
|
case "fileAsset":
|
|
g.Fgenf(w, "new pulumi.asset.FileAsset(%.v)", expr.Args[0])
|
|
case "invoke":
|
|
pkg, module, fn, diags := functionName(expr.Args[0])
|
|
contract.Assert(len(diags) == 0)
|
|
if module != "" {
|
|
module = "." + module
|
|
}
|
|
name := fmt.Sprintf("%s%s.%s", pkg, module, fn)
|
|
|
|
optionsBag := ""
|
|
if len(expr.Args) == 3 {
|
|
var buf bytes.Buffer
|
|
g.Fgenf(&buf, ", %.v", expr.Args[2])
|
|
optionsBag = buf.String()
|
|
}
|
|
|
|
g.Fgenf(w, "%s(%.v%v)", name, expr.Args[1], optionsBag)
|
|
case "length":
|
|
g.Fgenf(w, "%.20v.length", expr.Args[0])
|
|
case "lookup":
|
|
g.Fgenf(w, "%v[%v]", expr.Args[0], expr.Args[1])
|
|
if len(expr.Args) == 3 {
|
|
g.Fgenf(w, " || %v", expr.Args[2])
|
|
}
|
|
case "range":
|
|
g.genRange(w, expr, false)
|
|
case "readFile":
|
|
g.Fgenf(w, "fs.readFileSync(%v)", expr.Args[0])
|
|
case "readDir":
|
|
g.Fgenf(w, "fs.readDirSync(%v)", expr.Args[0])
|
|
case "split":
|
|
g.Fgenf(w, "%.20v.split(%v)", expr.Args[1], expr.Args[0])
|
|
case "toJSON":
|
|
g.Fgenf(w, "JSON.stringify(%v)", expr.Args[0])
|
|
default:
|
|
var rng hcl.Range
|
|
if expr.Syntax != nil {
|
|
rng = expr.Syntax.Range()
|
|
}
|
|
g.genNYI(w, "FunctionCallExpression: %v (%v)", expr.Name, rng)
|
|
}
|
|
}
|
|
|
|
func (g *generator) GenIndexExpression(w io.Writer, expr *model.IndexExpression) {
|
|
g.Fgenf(w, "%.20v[%.v]", expr.Collection, expr.Key)
|
|
}
|
|
|
|
func (g *generator) genStringLiteral(w io.Writer, v string) {
|
|
builder := strings.Builder{}
|
|
newlines := strings.Count(v, "\n")
|
|
if newlines == 0 || newlines == 1 && (v[0] == '\n' || v[len(v)-1] == '\n') {
|
|
// This string either does not contain newlines or contains a single leading or trailing newline, so we'll
|
|
// Generate a normal string literal. Quotes, backslashes, and newlines will be escaped in conformance with
|
|
// ECMA-262 11.8.4 ("String Literals").
|
|
builder.WriteRune('"')
|
|
for _, c := range v {
|
|
if c == '\n' {
|
|
builder.WriteString(`\n`)
|
|
} else {
|
|
if c == '"' || c == '\\' {
|
|
builder.WriteRune('\\')
|
|
}
|
|
builder.WriteRune(c)
|
|
}
|
|
}
|
|
builder.WriteRune('"')
|
|
} else {
|
|
// This string does contain newlines, so we'll Generate a template string literal. "${", backquotes, and
|
|
// backslashes will be escaped in conformance with ECMA-262 11.8.6 ("Template Literal Lexical Components").
|
|
runes := []rune(v)
|
|
builder.WriteRune('`')
|
|
for i, c := range runes {
|
|
switch c {
|
|
case '$':
|
|
if i < len(runes)-1 && runes[i+1] == '{' {
|
|
builder.WriteRune('\\')
|
|
}
|
|
case '`', '\\':
|
|
builder.WriteRune('\\')
|
|
}
|
|
builder.WriteRune(c)
|
|
}
|
|
builder.WriteRune('`')
|
|
}
|
|
|
|
g.Fgenf(w, "%s", builder.String())
|
|
}
|
|
|
|
func (g *generator) GenLiteralValueExpression(w io.Writer, expr *model.LiteralValueExpression) {
|
|
switch expr.Type() {
|
|
case model.BoolType:
|
|
g.Fgenf(w, "%v", expr.Value.True())
|
|
case model.NumberType:
|
|
bf := expr.Value.AsBigFloat()
|
|
if i, acc := bf.Int64(); acc == big.Exact {
|
|
g.Fgenf(w, "%d", i)
|
|
} else {
|
|
f, _ := bf.Float64()
|
|
g.Fgenf(w, "%g", f)
|
|
}
|
|
case model.StringType:
|
|
g.genStringLiteral(w, expr.Value.AsString())
|
|
default:
|
|
contract.Failf("unexpected literal type in GenLiteralValueExpression: %v (%v)", expr.Type(),
|
|
expr.SyntaxNode().Range())
|
|
}
|
|
}
|
|
|
|
func (g *generator) GenObjectConsExpression(w io.Writer, expr *model.ObjectConsExpression) {
|
|
if len(expr.Items) == 0 {
|
|
g.Fgen(w, "{}")
|
|
} else {
|
|
g.Fgen(w, "{")
|
|
g.Indented(func() {
|
|
for _, item := range expr.Items {
|
|
g.Fgenf(w, "\n%s", g.Indent)
|
|
if lit, isLit := item.Key.(*model.LiteralValueExpression); isLit {
|
|
if lit.Type() == model.StringType && isLegalIdentifier(lit.Value.AsString()) {
|
|
g.Fprint(w, lit.Value.AsString())
|
|
} else {
|
|
g.Fgenf(w, "%.v", lit)
|
|
}
|
|
} else {
|
|
g.Fgenf(w, "[%.v]", item.Key)
|
|
}
|
|
g.Fgenf(w, ": %.v,", item.Value)
|
|
}
|
|
})
|
|
g.Fgenf(w, "\n%s}", g.Indent)
|
|
}
|
|
}
|
|
|
|
func (g *generator) genRelativeTraversal(w io.Writer, traversal hcl.Traversal, parts []model.Traversable) {
|
|
for i, part := range traversal {
|
|
var key cty.Value
|
|
switch part := part.(type) {
|
|
case hcl.TraverseAttr:
|
|
key = cty.StringVal(part.Name)
|
|
case hcl.TraverseIndex:
|
|
key = part.Key
|
|
default:
|
|
contract.Failf("unexpected traversal part of type %T (%v)", part, part.SourceRange())
|
|
}
|
|
|
|
if model.IsOptionalType(model.GetTraversableType(parts[i])) {
|
|
g.Fgen(w, "?")
|
|
}
|
|
|
|
switch key.Type() {
|
|
case cty.String:
|
|
keyVal := key.AsString()
|
|
if isLegalIdentifier(keyVal) {
|
|
g.Fgenf(w, ".%s", keyVal)
|
|
} else {
|
|
g.Fgenf(w, "[%q]", keyVal)
|
|
}
|
|
case cty.Number:
|
|
idx, _ := key.AsBigFloat().Int64()
|
|
g.Fgenf(w, "[%d]", idx)
|
|
default:
|
|
g.Fgenf(w, "[%q]", key.AsString())
|
|
}
|
|
}
|
|
}
|
|
|
|
func (g *generator) GenRelativeTraversalExpression(w io.Writer, expr *model.RelativeTraversalExpression) {
|
|
g.Fgenf(w, "%.20v", expr.Source)
|
|
g.genRelativeTraversal(w, expr.Traversal, expr.Parts)
|
|
}
|
|
|
|
func (g *generator) GenScopeTraversalExpression(w io.Writer, expr *model.ScopeTraversalExpression) {
|
|
rootName := expr.RootName
|
|
if _, ok := expr.Parts[0].(*model.SplatVariable); ok {
|
|
rootName = "__item"
|
|
}
|
|
|
|
g.Fgen(w, rootName)
|
|
g.genRelativeTraversal(w, expr.Traversal.SimpleSplit().Rel, expr.Parts)
|
|
}
|
|
|
|
func (g *generator) GenSplatExpression(w io.Writer, expr *model.SplatExpression) {
|
|
g.Fgenf(w, "%.20v.map(__item => %.v)", expr.Source, expr.Each)
|
|
}
|
|
|
|
func (g *generator) GenTemplateExpression(w io.Writer, expr *model.TemplateExpression) {
|
|
if len(expr.Parts) == 1 {
|
|
if lit, ok := expr.Parts[0].(*model.LiteralValueExpression); ok && lit.Type() == model.StringType {
|
|
g.GenLiteralValueExpression(w, lit)
|
|
return
|
|
}
|
|
}
|
|
|
|
g.Fgen(w, "`")
|
|
for _, expr := range expr.Parts {
|
|
if lit, ok := expr.(*model.LiteralValueExpression); ok && lit.Type() == model.StringType {
|
|
g.Fgen(w, lit.Value.AsString())
|
|
} else {
|
|
g.Fgenf(w, "${%.v}", expr)
|
|
}
|
|
}
|
|
g.Fgen(w, "`")
|
|
}
|
|
|
|
func (g *generator) GenTemplateJoinExpression(w io.Writer, expr *model.TemplateJoinExpression) {
|
|
g.genNYI(w, "TemplateJoinExpression")
|
|
}
|
|
|
|
func (g *generator) GenTupleConsExpression(w io.Writer, expr *model.TupleConsExpression) {
|
|
switch len(expr.Expressions) {
|
|
case 0:
|
|
g.Fgen(w, "[]")
|
|
case 1:
|
|
g.Fgenf(w, "[%.v]", expr.Expressions[0])
|
|
default:
|
|
g.Fgen(w, "[")
|
|
g.Indented(func() {
|
|
for _, v := range expr.Expressions {
|
|
g.Fgenf(w, "\n%s%.v,", g.Indent, v)
|
|
}
|
|
})
|
|
g.Fgen(w, "\n", g.Indent, "]")
|
|
}
|
|
}
|
|
|
|
func (g *generator) GenUnaryOpExpression(w io.Writer, expr *model.UnaryOpExpression) {
|
|
opstr, precedence := "", g.GetPrecedence(expr)
|
|
switch expr.Operation {
|
|
case hclsyntax.OpLogicalNot:
|
|
opstr = "!"
|
|
case hclsyntax.OpNegate:
|
|
opstr = "-"
|
|
}
|
|
g.Fgenf(w, "%[2]v%.[1]*[3]v", precedence, opstr, expr.Operand)
|
|
}
|