// Copyright 2016-2020, Pulumi Corporation. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package dotnet import ( "fmt" "io" "math/big" "strings" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" "github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/model" "github.com/pulumi/pulumi/pkg/v3/codegen/pcl" "github.com/pulumi/pulumi/pkg/v3/codegen/schema" "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" "github.com/zclconf/go-cty/cty" ) type nameInfo int func (nameInfo) Format(name string) string { return makeValidIdentifier(name) } func (g *generator) rewriteExpression(expr model.Expression, typ model.Type, rewriteApplies bool) model.Expression { expr = pcl.RewritePropertyReferences(expr) var diags hcl.Diagnostics if rewriteApplies { expr, diags = pcl.RewriteApplies(expr, nameInfo(0), !g.asyncInit) } expr, convertDiags := pcl.RewriteConversions(expr, typ) diags = diags.Extend(convertDiags) if g.asyncInit { expr = g.awaitInvokes(expr) } else { expr = g.outputInvokes(expr) } g.diagnostics = g.diagnostics.Extend(diags) return expr } // lowerExpression amends the expression with intrinsics for C# generation. func (g *generator) lowerExpression(expr model.Expression, typ model.Type) model.Expression { rewriteApplies := true return g.rewriteExpression(expr, typ, rewriteApplies) } // lowerExpressionWithoutApplies is the same as lowerExpression // but without rewriting applies. Made especially for function invokes that are returning outputs func (g *generator) lowerExpressionWithoutApplies(expr model.Expression, typ model.Type) model.Expression { rewriteApplies := false return g.rewriteExpression(expr, typ, rewriteApplies) } // awaitInvokes wraps each call to `invoke` with a call to the `await` intrinsic. This rewrite should only be used // if we are generating an async Initialize, in which case the apply rewriter should also be configured not to treat // promises as eventuals. Note that this depends on the fact that invokes are the only way to introduce promises // in to a Pulumi program; if this changes in the future, this transform will need to be applied in a more general way // (e.g. by the apply rewriter). func (g *generator) awaitInvokes(x model.Expression) model.Expression { contract.Assertf(g.asyncInit, "awaitInvokes can be used only if we are generating an async Initialize") rewriter := func(x model.Expression) (model.Expression, hcl.Diagnostics) { // Ignore the node if it is not a call to invoke. call, ok := x.(*model.FunctionCallExpression) if !ok || call.Name != pcl.Invoke { return x, nil } if _, isPromise := call.Type().(*model.PromiseType); isPromise { return newAwaitCall(call), nil } return call, nil } x, diags := model.VisitExpression(x, model.IdentityVisitor, rewriter) contract.Assertf(len(diags) == 0, "unexpected diagnostics: %v", diags) return x } // outputInvokes wraps each call to `invoke` with a call to the `output` intrinsic. This rewrite should only be used if // resources are instantiated within a stack constructor, where `await` operator is not available. We want to avoid the // nastiness of working with raw `Task` and wrap it into Pulumi's Output immediately to be able to `Apply` on it. // Note that this depends on the fact that invokes are the only way to introduce promises // in to a Pulumi program; if this changes in the future, this transform will need to be applied in a more general way // (e.g. by the apply rewriter). func (g *generator) outputInvokes(x model.Expression) model.Expression { rewriter := func(x model.Expression) (model.Expression, hcl.Diagnostics) { // Ignore the node if it is not a call to invoke. call, ok := x.(*model.FunctionCallExpression) if !ok || call.Name != pcl.Invoke { return x, nil } if call.Type() == model.DynamicType { // ignore if the return type of the invoke is dynamic // this means that we are working with an unknown invoke return x, nil } _, isOutput := call.Type().(*model.OutputType) if isOutput { return x, nil } _, isPromise := call.Type().(*model.PromiseType) contract.Assertf(isPromise, "invoke should return a promise, got %v", call.Type()) return newOutputCall(call), nil } x, diags := model.VisitExpression(x, model.IdentityVisitor, rewriter) contract.Assertf(len(diags) == 0, "unexpected diagnostics: %v", diags) return x } func (g *generator) GetPrecedence(expr model.Expression) int { // TODO(msh): Current values copied from Node, update based on // https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/ 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 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) g.Fgenf(w, " => %v", expr.Body) default: g.Fgen(w, "values =>\n") g.Fgenf(w, "%s{\n", g.Indent) g.Indented(func() { for i, p := range expr.Signature.Parameters { g.Fgenf(w, "%svar %s = values.Item%d;\n", g.Indent, p.Name, i+1) } g.Fgenf(w, "%sreturn %v;\n", g.Indent, expr.Body) }) g.Fgenf(w, "%s}", g.Indent) } } 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.Select((value, i) => new { Key = i.ToString(), Value = pair.Value })", expr.Collection) } case *model.MapType: if expr.KeyVariable == nil { g.Fgenf(w, "(%.v).Values", expr.Collection) } else { g.Fgenf(w, "%.20v.Select(pair => new { pair.Key, pair.Value })", expr.Collection) } } switch expr.Type().(type) { case *model.ListType: // the result of the expression is a list if expr.Condition != nil { g.Fgenf(w, ".Where(%s => %.v)", expr.ValueVariable.Name, expr.Condition) } g.Fgenf(w, ".Select(%s => \n", expr.ValueVariable.Name) g.Fgenf(w, "%s{\n", g.Indent) g.Indented(func() { g.Fgenf(w, "%sreturn %v;", g.Indent, expr.Value) }) g.Fgen(w, "\n") // .ToList() is added so that the expressions returns `List<T> // which can be implicitly converted to InputList<T> g.Fgenf(w, "%s}).ToList()", g.Indent) case *model.MapType: // the result of the expression is a dictionary g.Fgen(w, ".ToDictionary(item => {\n") g.Indented(func() { if expr.KeyVariable != nil && pcl.VariableAccessed(expr.KeyVariable.Name, expr.Key) { g.Fgenf(w, "%svar %s = item.Key;\n", g.Indent, expr.KeyVariable.Name) } if expr.ValueVariable != nil && pcl.VariableAccessed(expr.ValueVariable.Name, expr.Key) { g.Fgenf(w, "%svar %s = item.Value;\n", g.Indent, expr.ValueVariable.Name) } g.Fgenf(w, "%sreturn %s;\n", g.Indent, expr.Key) }) g.Fgenf(w, "%s}, item => {\n", g.Indent) g.Indented(func() { if expr.KeyVariable != nil && pcl.VariableAccessed(expr.KeyVariable.Name, expr.Value) { g.Fgenf(w, "%svar %s = item.Key;\n", g.Indent, expr.KeyVariable.Name) } if expr.ValueVariable != nil && pcl.VariableAccessed(expr.ValueVariable.Name, expr.Value) { g.Fgenf(w, "%svar %s = item.Value;\n", g.Indent, expr.ValueVariable.Name) } g.Fgenf(w, "%sreturn %v;\n", g.Indent, expr.Value) }) g.Fgenf(w, "%s})", g.Indent) } } 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 := pcl.ParseApplyCall(expr) if len(applyArgs) == 1 { // If we only have a single output, just generate a normal `.Apply` g.Fgenf(w, "%.v.Apply(%.v)", applyArgs[0], then) } else { // Otherwise, generate a call to `Output.Tuple().Apply()`. g.Fgen(w, "Output.Tuple(") for i, o := range applyArgs { if i > 0 { g.Fgen(w, ", ") } g.Fgenf(w, "%.v", o) } g.Fgenf(w, ").Apply(%.v)", then) } } func (g *generator) genRange(w io.Writer, call *model.FunctionCallExpression, entries bool) { g.genNYI(w, "Range %.v %.v", call, entries) } var functionNamespaces = map[string][]string{ "assetArchive": {"System.Collections.Generic"}, "readDir": {"System.IO", "System.Linq"}, "readFile": {"System.IO"}, "cwd": {"System.IO"}, "filebase64": {"System", "System.IO"}, "filebase64sha256": {"System", "System.IO", "System.Security.Cryptography", "System.Text"}, "toJSON": {"System.Text.Json", "System.Collections.Generic"}, "toBase64": {"System"}, "fromBase64": {"System"}, "sha1": {"System.Security.Cryptography", "System.Text"}, "singleOrNone": {"System.Linq"}, } func (g *generator) genFunctionUsings(x *model.FunctionCallExpression) []string { if x.Name != pcl.Invoke { return functionNamespaces[x.Name] } pkg, _ := g.functionName(x.Args[0]) return []string{fmt.Sprintf("%s = Pulumi.%[1]s", pkg)} } func (g *generator) genSafeEnum(w io.Writer, to *model.EnumType) func(member *schema.Enum) { return func(member *schema.Enum) { // We know the enum value at the call site, so we can directly stamp in a // valid enum instance. We don't need to convert. pkg, name := enumName(to) contract.Assertf(pkg != "", "pkg cannot be empty") contract.Assertf(name != "", "name cannot be empty") memberTag := member.Name if memberTag == "" { memberTag = member.Value.(string) } memberTag, err := makeSafeEnumName(memberTag, name) contract.AssertNoErrorf(err, "Enum is invalid") g.Fgenf(w, "%s.%s.%s", pkg, name, memberTag) } } func enumName(enum *model.EnumType) (string, string) { components := strings.Split(enum.Token, ":") contract.Assertf(len(components) == 3, "malformed token %v", enum.Token) enumName := tokenToName(enum.Token) e, ok := pcl.GetSchemaForType(enum) if !ok { return "", "" } et := e.(*schema.EnumType) def, err := et.PackageReference.Definition() contract.AssertNoErrorf(err, "error loading definition for package %q", et.PackageReference.Name()) namespaceMap := def.Language["csharp"].(CSharpPackageInfo).Namespaces namespace := namespaceName(namespaceMap, components[0]) if components[1] != "" && components[1] != "index" { namespace += "." + namespaceName(namespaceMap, components[1]) } return namespace, enumName } func (g *generator) genIntrensic(w io.Writer, from model.Expression, to model.Type) { to = pcl.LowerConversion(from, to) output, isOutput := to.(*model.OutputType) if isOutput { to = output.ElementType } switch to := to.(type) { case *model.EnumType: pkg, name := enumName(to) if pkg == "" || name == "" { // Something has gone wrong. Produce a best effort result. g.Fgenf(w, "%.v", from) return } var convertFn string switch { case to.Type.Equals(model.StringType): convertFn = fmt.Sprintf("System.Enum.Parse<%s.%s>", pkg, name) default: panic(fmt.Sprintf( "Unsafe enum conversions from type %s not implemented yet: %s => %s", from.Type(), from, to)) } if isOutput { g.Fgenf(w, "%.v.Apply(%s)", from, convertFn) } else { diag := pcl.GenEnum(to, from, g.genSafeEnum(w, to), func(from model.Expression) { g.Fgenf(w, "%s(%v)", convertFn, from) }) if diag != nil { g.diagnostics = append(g.diagnostics, diag) } } default: g.Fgenf(w, "%.v", from) // <- probably wrong w.r.t. precedence } } func (g *generator) genEntries(w io.Writer, expr *model.FunctionCallExpression) { switch model.ResolveOutputs(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.Select((v, k) => new { Key = k, Value = v })", expr.Args[0]) case *model.MapType, *model.ObjectType: g.Fgenf(w, "%.20v.Select(pair => new { pair.Key, pair.Value })", expr.Args[0]) } } func (g *generator) withinAwaitBlock(run func()) { if g.insideAwait { // already inside await block? // only run the function run() } else { // not inside await? flag it as true, run the function, // then set it back to false g.insideAwait = true run() g.insideAwait = false } } func (g *generator) GenFunctionCallExpression(w io.Writer, expr *model.FunctionCallExpression) { switch expr.Name { case pcl.IntrinsicConvert: switch arg := expr.Args[0].(type) { case *model.ObjectConsExpression: g.genObjectConsExpression(w, arg, expr.Type()) default: g.genIntrensic(w, expr.Args[0], expr.Signature.ReturnType) } case pcl.IntrinsicApply: switch expr.Args[0].(type) { case *model.ScopeTraversalExpression: traversal := expr.Args[0].(*model.ScopeTraversalExpression) if len(traversal.Parts) == 1 { _, isInvoke := g.functionInvokes[traversal.RootName] if isInvoke { switch expr.Args[1].(type) { case *model.AnonymousFunctionExpression: anonFunction := expr.Args[1].(*model.AnonymousFunctionExpression) g.Fgenf(w, "%v", anonFunction.Body) return } } } } g.genApply(w, expr) case intrinsicAwait: g.withinAwaitBlock(func() { g.Fgenf(w, "await %.17v", expr.Args[0]) }) case intrinsicOutput: // if we are calling Output.Create(FuncInvokeAsync()) // then we can simplify to just FuncInvoke() which already returns Output if funcExpr, isFunc := expr.Args[0].(*model.FunctionCallExpression); isFunc && funcExpr.Name == pcl.Invoke { _, fullFunctionName := g.functionName(funcExpr.Args[0]) g.Fprintf(w, "%s.Invoke(", fullFunctionName) functionParts := strings.Split(fullFunctionName, ".") functionName := functionParts[len(functionParts)-1] innerFunc, isFunc := funcExpr.Args[1].(*model.FunctionCallExpression) if isFunc && innerFunc.Name == pcl.IntrinsicConvert { switch arg := innerFunc.Args[0].(type) { case *model.ObjectConsExpression: g.withinFunctionInvoke(func() { useImplicitTypeName := g.generateOptions.implicitResourceArgsTypeName inputTypeName := functionName + "InvokeArgs" destTypeName := strings.ReplaceAll(fullFunctionName, functionName, inputTypeName) g.genObjectConsExpressionWithTypeName(w, arg, destTypeName, useImplicitTypeName, pcl.SortedFunctionParameters(funcExpr)) }) default: g.genIntrensic(w, funcExpr.Args[0], expr.Signature.ReturnType) } } else { if objectExpr, ok := funcExpr.Args[1].(*model.ObjectConsExpression); ok { g.withinFunctionInvoke(func() { useImplicitTypeName := g.generateOptions.implicitResourceArgsTypeName inputTypeName := functionName + "InvokeArgs" destTypeName := strings.ReplaceAll(fullFunctionName, functionName, inputTypeName) g.genObjectConsExpressionWithTypeName(w, objectExpr, destTypeName, useImplicitTypeName, pcl.SortedFunctionParameters(funcExpr)) }) } else { g.Fgenf(w, "%v", funcExpr.Args[1]) } } g.Fprint(w, ")") } else { g.Fgenf(w, "Output.Create(%.v)", expr.Args[0]) } case "element": g.Fgenf(w, "%.20v[%.v]", expr.Args[0], expr.Args[1]) case "entries": g.genEntries(w, expr) case "fileArchive": g.Fgenf(w, "new FileArchive(%.v)", expr.Args[0]) case "remoteArchive": g.Fgenf(w, "new RemoteArchive(%.v)", expr.Args[0]) case "assetArchive": g.Fgen(w, "new AssetArchive(") g.genDictionary(w, expr.Args[0].(*model.ObjectConsExpression), "AssetOrArchive") g.Fgen(w, ")") case "fileAsset": g.Fgenf(w, "new FileAsset(%.v)", expr.Args[0]) case "stringAsset": g.Fgenf(w, "new StringAsset(%.v)", expr.Args[0]) case "remoteAsset": g.Fgenf(w, "new RemoteAsset(%.v)", expr.Args[0]) case "filebase64": // Assuming the existence of the following helper method located earlier in the preamble g.Fgenf(w, "ReadFileBase64(%v)", expr.Args[0]) case "filebase64sha256": // Assuming the existence of the following helper method located earlier in the preamble g.Fgenf(w, "ComputeFileBase64Sha256(%v)", expr.Args[0]) case "notImplemented": g.Fgenf(w, "NotImplemented(%v)", expr.Args[0]) case "singleOrNone": g.Fgenf(w, "Enumerable.Single(%v)", expr.Args[0]) case pcl.Invoke: _, fullFunctionName := g.functionName(expr.Args[0]) functionParts := strings.Split(fullFunctionName, ".") functionName := functionParts[len(functionParts)-1] if g.insideAwait { g.Fprintf(w, "%s.InvokeAsync(", fullFunctionName) } else { g.Fprintf(w, "%s.Invoke(", fullFunctionName) } innerFunc, isFunc := expr.Args[1].(*model.FunctionCallExpression) if isFunc && innerFunc.Name == pcl.IntrinsicConvert { // function has been "lowered" i.e. rewritten with __convert switch arg := innerFunc.Args[0].(type) { case *model.ObjectConsExpression: g.withinFunctionInvoke(func() { useImplicitTypeName := g.generateOptions.implicitResourceArgsTypeName inputTypeName := functionName + "InvokeArgs" if g.insideAwait { inputTypeName = functionName + "Args" } destTypeName := strings.ReplaceAll(fullFunctionName, functionName, inputTypeName) g.genObjectConsExpressionWithTypeName(w, arg, destTypeName, useImplicitTypeName, pcl.SortedFunctionParameters(expr)) }) default: g.genIntrensic(w, expr.Args[0], expr.Signature.ReturnType) } } else { // function has not been rewritten switch arg := expr.Args[1].(type) { case *model.ObjectConsExpression: useImplicitTypeName := true destTypeName := "Irrelevant" g.genObjectConsExpressionWithTypeName(w, arg, destTypeName, useImplicitTypeName, pcl.SortedFunctionParameters(expr)) default: g.genIntrensic(w, expr.Args[0], expr.Signature.ReturnType) } } g.Fprint(w, ")") case "join": g.Fgenf(w, "string.Join(%v, %v)", expr.Args[0], expr.Args[1]) 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, "File.ReadAllText(%v)", expr.Args[0]) case "readDir": g.Fgenf(w, "Directory.GetFiles(%.v).Select(Path.GetFileName)", expr.Args[0]) case "secret": g.Fgenf(w, "Output.CreateSecret(%v)", expr.Args[0]) case "unsecret": g.Fgenf(w, "Output.Unsecret(%v)", expr.Args[0]) case "split": g.Fgenf(w, "%.20v.Split(%v)", expr.Args[1], expr.Args[0]) case "toBase64": g.Fgenf(w, "Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(%v))", expr.Args[0]) case "fromBase64": g.Fgenf(w, "System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(%v))", expr.Args[0]) case "toJSON": g.Fgen(w, "JsonSerializer.Serialize(") g.genDictionaryOrTuple(w, expr.Args[0]) g.Fgen(w, ")") case "sha1": // Assuming the existence of the following helper method located earlier in the preamble g.Fgenf(w, "ComputeSHA1(%v)", expr.Args[0]) case "stack": g.Fgen(w, "Deployment.Instance.StackName") case "project": g.Fgen(w, "Deployment.Instance.ProjectName") case "cwd": g.Fgenf(w, "Directory.GetCurrentDirectory()") default: g.genNYI(w, "call %v", expr.Name) } } func (g *generator) genDictionaryOrTuple(w io.Writer, expr model.Expression) { switch expr := expr.(type) { case *model.ObjectConsExpression: g.genDictionary(w, expr, "object?") case *model.TupleConsExpression: g.Fgen(w, "new[]\n") g.Fgenf(w, "%[1]s{\n", g.Indent) g.Indented(func() { for _, v := range expr.Expressions { g.Fgenf(w, "%s", g.Indent) g.genDictionaryOrTuple(w, v) g.Fgen(w, ",\n") } }) g.Fgenf(w, "%s}", g.Indent) default: g.Fgenf(w, "%.v", expr) } } func (g *generator) genDictionary(w io.Writer, expr *model.ObjectConsExpression, valueType string) { g.Fgenf(w, "new Dictionary<string, %s>\n", valueType) g.Fgenf(w, "%s{\n", g.Indent) g.Indented(func() { for _, item := range expr.Items { g.Fgenf(w, "%s[%.v] = ", g.Indent, item.Key) g.genDictionaryOrTuple(w, item.Value) g.Fgen(w, ",\n") } }) g.Fgenf(w, "%s}", g.Indent) } func (g *generator) GenIndexExpression(w io.Writer, expr *model.IndexExpression) { g.Fgenf(w, "%.20v[%.v]", expr.Collection, expr.Key) } func (g *generator) escapeString(v string, verbatim, expressions bool) string { builder := strings.Builder{} for _, c := range v { if verbatim { if c == '"' { builder.WriteRune('"') } } else { if c == '"' || c == '\\' { builder.WriteRune('\\') } } if expressions && (c == '{' || c == '}') { builder.WriteRune(c) } builder.WriteRune(c) } return builder.String() } func (g *generator) genStringLiteral(w io.Writer, v string) { newlines := strings.Contains(v, "\n") if !newlines { // This string does not contain newlines so we'll generate a regular string literal. Quotes and backslashes // will be escaped in conformance with // https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/lexical-structure g.Fgen(w, "\"") g.Fgen(w, g.escapeString(v, false, false)) g.Fgen(w, "\"") } else { // This string does contain newlines, so we'll generate a verbatim string literal. Quotes will be escaped // in conformance with // https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/lexical-structure g.Fgen(w, "@\"") g.Fgen(w, g.escapeString(v, true, false)) g.Fgen(w, "\"") } } func (g *generator) GenLiteralValueExpression(w io.Writer, expr *model.LiteralValueExpression) { typ := expr.Type() if cns, ok := typ.(*model.ConstType); ok { typ = cns.Type } switch typ { case model.BoolType: g.Fgenf(w, "%v", expr.Value.True()) case model.NoneType: g.Fgen(w, "null") 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) { switch argType := expr.Type().(type) { case *model.ObjectType: if len(argType.Annotations) > 0 { if configMetadata, ok := argType.Annotations[0].(*ObjectTypeFromConfigMetadata); ok { fullTypeName := fmt.Sprintf("Components.%sArgs.%s", configMetadata.ComponentName, configMetadata.TypeName) g.genObjectConsExpressionWithTypeName(w, expr, fullTypeName, false, nil) return } } } g.genObjectConsExpression(w, expr, expr.Type()) } func (g *generator) genObjectConsExpression(w io.Writer, expr *model.ObjectConsExpression, destType model.Type) { if len(expr.Items) == 0 { g.Fgenf(w, "null") return } destTypeName := g.argumentTypeName(expr, destType) g.genObjectConsExpressionWithTypeName(w, expr, destTypeName, false, nil) } func propertyNameOverrides(exprType model.Type) map[string]string { overrides := make(map[string]string) schemaType, ok := pcl.GetSchemaForType(exprType) if !ok { return overrides } switch arg := schemaType.(type) { case *schema.ObjectType: for _, property := range arg.Properties { foundOverride := false if csharp, ok := property.Language["csharp"]; ok { if options, ok := csharp.(CSharpPropertyInfo); ok { overrides[property.Name] = options.Name foundOverride = true } } if !foundOverride { overrides[property.Name] = property.Name } } } return overrides } func resolvePropertyName(property string, overrides map[string]string) string { foundOverride, ok := overrides[property] if ok { return propertyName(foundOverride) } return propertyName(property) } func unwrapIntrinsicConvert(expr model.Expression) model.Expression { if call, ok := expr.(*model.FunctionCallExpression); ok && call.Name == pcl.IntrinsicConvert { return call.Args[0] } return expr } func isEmptyList(expr model.Expression) bool { expr = unwrapIntrinsicConvert(expr) if list, ok := expr.(*model.TupleConsExpression); ok { return len(list.Expressions) == 0 } return false } func objectKey(item model.ObjectConsItem) string { switch key := item.Key.(type) { case *model.LiteralValueExpression: return key.Value.AsString() case *model.TemplateExpression: // assume a template expression has one constant part that is a LiteralValueExpression if len(key.Parts) == 1 { if literal, ok := key.Parts[0].(*model.LiteralValueExpression); ok { return literal.Value.AsString() } } } return "" } func (g *generator) genObjectConsExpressionWithTypeName( w io.Writer, expr *model.ObjectConsExpression, destTypeName string, implicitTypeName bool, multiArguments []*schema.Property, ) { if len(expr.Items) == 0 { return } if len(multiArguments) > 0 { pcl.GenerateMultiArguments(g.Formatter, w, "null", expr, multiArguments) return } typeName := destTypeName if typeName != "" { if implicitTypeName { g.Fgenf(w, "new()") } else { g.Fgenf(w, "new %s", typeName) } propertyNames := propertyNameOverrides(expr.Type()) g.Fgenf(w, "\n%s{\n", g.Indent) g.Indented(func() { for _, item := range expr.Items { g.Fgenf(w, "%s", g.Indent) propertyKey := objectKey(item) g.Fprint(w, resolvePropertyName(propertyKey, propertyNames)) if g.usingDefaultListInitializer() && isEmptyList(item.Value) { g.Fgen(w, " = new() { },\n") } else { g.Fgenf(w, " = %.v,\n", item.Value) } } }) g.Fgenf(w, "%s}", g.Indent) } else { g.Fgenf(w, "\n%s{\n", g.Indent) g.Indented(func() { for _, item := range expr.Items { g.Fgenf(w, "%s{ %.v, %.v },\n", g.Indent, item.Key, item.Value) } }) g.Fgenf(w, "%s}", g.Indent) } } func (g *generator) genRelativeTraversal(w io.Writer, traversal hcl.Traversal, parts []model.Traversable, objType *schema.ObjectType, ) { for i, part := range traversal { var key cty.Value switch part := part.(type) { case hcl.TraverseAttr: key = cty.StringVal(part.Name) if objType != nil { if p, ok := objType.Property(part.Name); ok { if info, ok := p.Language["csharp"].(CSharpPropertyInfo); ok && info.Name != "" { key = cty.StringVal(info.Name) } } } case hcl.TraverseIndex: key = part.Key default: contract.Failf("unexpected traversal part of type %T (%v)", part, part.SourceRange()) } switch key.Type() { case cty.String: if model.IsOptionalType(model.GetTraversableType(parts[i])) { g.Fgen(w, "?") } g.Fgenf(w, ".%s", propertyName(key.AsString())) case cty.Number: idx, _ := key.AsBigFloat().Int64() g.Fgenf(w, "[%d]", idx) default: contract.Failf("unexpected traversal key of type %T (%v)", key, 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, nil) } func (g *generator) schemaTypeName(schemaType *schema.ObjectType) string { fullyQualifiedTypeName := schemaType.Token nameParts := strings.Split(fullyQualifiedTypeName, ":") return Title(nameParts[len(nameParts)-1]) } func (g *generator) withinFunctionInvoke(run func()) { if g.insideFunctionInvoke { // already inside this block? // just run the function run() } else { // not inside function invoke? // set it to true first, run, then set it back to false g.insideFunctionInvoke = true run() g.insideFunctionInvoke = false } } func (g *generator) GenScopeTraversalExpression(w io.Writer, expr *model.ScopeTraversalExpression) { rootName := makeValidIdentifier(expr.RootName) if g.isComponent { configVars := map[string]*pcl.ConfigVariable{} for _, configVar := range g.program.ConfigVariables() { configVars[configVar.Name()] = configVar } if _, isConfig := configVars[expr.RootName]; isConfig { if _, configReference := expr.Parts[0].(*pcl.ConfigVariable); configReference { rootName = "args." + Title(expr.RootName) } } } if _, ok := expr.Parts[0].(*model.SplatVariable); ok { rootName = "__item" } g.Fgen(w, rootName) invokedFunctionSchema, isFunctionInvoke := g.functionInvokes[rootName] if isFunctionInvoke && !g.asyncInit && len(expr.Parts) > 1 { lambdaArg := "invoke" if invokedFunctionSchema.ReturnType != nil { if objectType, ok := invokedFunctionSchema.ReturnType.(*schema.ObjectType); ok && objectType != nil { lambdaArg = LowerCamelCase(g.schemaTypeName(objectType)) } } // Assume invokes are returning Output<T> instead of Task<T> g.Fgenf(w, ".Apply(%s => %s", lambdaArg, lambdaArg) } var objType *schema.ObjectType if resource, ok := expr.Parts[0].(*pcl.Resource); ok { if schemaType, ok := pcl.GetSchemaForType(resource.InputType); ok { objType, _ = schemaType.(*schema.ObjectType) } } g.genRelativeTraversal(w, expr.Traversal.SimpleSplit().Rel, expr.Parts, objType) if isFunctionInvoke && !g.asyncInit && len(expr.Parts) > 1 { g.Fgenf(w, ")") } } func (g *generator) GenSplatExpression(w io.Writer, expr *model.SplatExpression) { g.Fgenf(w, "%.20v.Select(__item => %.v).ToList()", expr.Source, expr.Each) } func (g *generator) GenTemplateExpression(w io.Writer, expr *model.TemplateExpression) { multiLine := false expressions := false for _, expr := range expr.Parts { if lit, ok := expr.(*model.LiteralValueExpression); ok && model.StringType.AssignableFrom(lit.Type()) { if strings.Contains(lit.Value.AsString(), "\n") { multiLine = true } } else { expressions = true } } if multiLine { g.Fgen(w, "@") } if expressions { g.Fgen(w, "$") } g.Fgen(w, "\"") for _, expr := range expr.Parts { if lit, ok := expr.(*model.LiteralValueExpression); ok && model.StringType.AssignableFrom(lit.Type()) { g.Fgen(w, g.escapeString(lit.Value.AsString(), multiLine, expressions)) } else { g.Fgenf(w, "{%.v}", expr) } } g.Fgen(w, "\"") } func (g *generator) GenTemplateJoinExpression(w io.Writer, expr *model.TemplateJoinExpression) { g.genNYI(w, "TemplateJoinExpression") } // Removes duplicate strings. Useful when collecting a distinct set of imports func removeDuplicates(inputs []string) []string { distinctInputs := make([]string, 0) seenTexts := make(map[string]bool) for _, input := range inputs { if _, seen := seenTexts[input]; !seen { seenTexts[input] = true distinctInputs = append(distinctInputs, input) } } return distinctInputs } func (g *generator) isListOfDifferentTypes(expr *model.TupleConsExpression) bool { switch expr.Type().(type) { case *model.TupleType: tupleType := expr.Type().(*model.TupleType) typeNames := make([]string, 0) for _, elemType := range tupleType.ElementTypes { if schemaType, ok := pcl.GetSchemaForType(elemType); ok { if objectType, ok := schemaType.(*schema.ObjectType); ok { typeName := g.schemaTypeName(objectType) typeNames = append(typeNames, typeName) } } } return len(removeDuplicates(typeNames)) > 1 } return false } func (g *generator) GenTupleConsExpression(w io.Writer, expr *model.TupleConsExpression) { switch len(expr.Expressions) { case 0: g.Fgenf(w, "%s {}", g.listInitializer) default: if !g.isListOfDifferentTypes(expr) { // only generate a list initializer when we don't have a list of union types // because list of a union is mapped to InputList<object> // which means new[] will not work because type-inference won't // know the type of the array beforehand g.Fgenf(w, "%s", g.listInitializer) } g.Fgenf(w, "\n%s{", g.Indent) g.Indented(func() { for _, v := range expr.Expressions { g.Fgenf(w, "\n%s%.v,", g.Indent, v) } }) g.Fgenf(w, "\n%s}", 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) }