pulumi/pkg/codegen/go/gen_program_expressions.go

1245 lines
38 KiB
Go
Raw Permalink Normal View History

2020-05-21 17:23:33 +00:00
package gen
import (
"bytes"
2020-05-21 17:23:33 +00:00
"fmt"
"io"
"math/big"
"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/pcl"
"github.com/pulumi/pulumi/pkg/v3/codegen/schema"
"github.com/pulumi/pulumi/sdk/v3/go/common/slice"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
2020-05-21 17:23:33 +00:00
"github.com/zclconf/go-cty/cty"
)
const keywordRange = "range"
2020-05-21 17:23:33 +00:00
func (g *generator) GetPrecedence(expr model.Expression) int {
// TODO: Current values copied from Node, update based on
// https://golang.org/ref/spec
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 {
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
}
// GenAnonymousFunctionExpression generates code for an AnonymousFunctionExpression.
func (g *generator) GenAnonymousFunctionExpression(w io.Writer, expr *model.AnonymousFunctionExpression) {
g.genAnonymousFunctionExpression(w, expr, nil, false)
2020-06-19 00:17:51 +00:00
}
func (g *generator) genAnonymousFunctionExpression(
w io.Writer,
expr *model.AnonymousFunctionExpression,
bodyPreamble []string,
inApply bool,
2020-06-19 00:17:51 +00:00
) {
g.Fgenf(w, "func(")
leadingSep := ""
for _, param := range expr.Signature.Parameters {
isInput := isInputty(param.Type)
2021-08-19 07:22:27 +00:00
g.Fgenf(w, "%s%s %s", leadingSep, makeValidIdentifier(param.Name), g.argumentTypeName(nil, param.Type, isInput))
leadingSep = ", "
}
retType := expr.Signature.ReturnType
if inApply {
retType = model.ResolveOutputs(retType)
}
retTypeName := g.argumentTypeName(nil, retType, false)
g.Fgenf(w, ") (%s, error) {\n", retTypeName)
2020-06-19 00:17:51 +00:00
for _, decl := range bodyPreamble {
g.Fgenf(w, "%s\n", decl)
}
body, temps := g.lowerExpression(expr.Body, retType)
g.genTempsMultiReturn(w, temps, retTypeName)
2022-09-16 23:12:29 +00:00
// g.Fgenf(w, "return %v, nil", body)
// fromBase64 special case
if b, ok := body.(*model.FunctionCallExpression); ok && b.Name == fromBase64Fn {
g.Fgenf(w, "value, _ := %v\n", b)
g.Fgenf(w, "return pulumi.String(value), nil")
} else if strings.HasPrefix(retTypeName, "pulumi") {
g.Fgenf(w, "return %s(%v), nil", retTypeName, body)
} else {
g.Fgenf(w, "return %v, nil", body)
}
g.Fgenf(w, "\n}")
2020-05-21 17:23:33 +00:00
}
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
}
2020-05-21 17:23:33 +00:00
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) {
// Ternary expressions are not supported in go so we need to allocate temp variables in the parent scope.
// This is handled by lower expression and rewriteTernaries
contract.Failf("unlowered conditional expression @ %v", expr.SyntaxNode().Range())
}
2020-05-21 17:23:33 +00:00
// GenForExpression generates code for a ForExpression.
func (g *generator) GenForExpression(w io.Writer, expr *model.ForExpression) {
g.genNYI(w, "For expression")
}
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.
enumName := tokenToName(to.Token)
memberTag := member.Name
if memberTag == "" {
memberTag = member.Value.(string)
}
memberTag, err := makeSafeEnumName(memberTag, enumName)
contract.AssertNoErrorf(err, "Enum is invalid")
pkg, mod, _, _ := pcl.DecomposeToken(to.Token, to.SyntaxNode().Range())
mod = g.getModOrAlias(pkg, mod, mod)
g.Fgenf(w, "%s.%s", mod, memberTag)
}
}
2020-05-21 17:23:33 +00:00
func (g *generator) GenFunctionCallExpression(w io.Writer, expr *model.FunctionCallExpression) {
switch expr.Name {
case pcl.IntrinsicConvert:
from := expr.Args[0]
to := pcl.LowerConversion(from, expr.Signature.ReturnType)
output, isOutput := to.(*model.OutputType)
if isOutput {
to = output.ElementType
}
switch to := to.(type) {
case *model.EnumType:
var underlyingType string
switch {
case to.Type.Equals(model.StringType):
underlyingType = "string"
default:
panic(fmt.Sprintf(
"Unsafe enum conversions from type %s not implemented yet: %s => %s",
from.Type(), from, to))
}
pkg, mod, typ, _ := pcl.DecomposeToken(to.Token, to.SyntaxNode().Range())
mod = g.getModOrAlias(pkg, mod, mod)
enumTag := fmt.Sprintf("%s.%s", mod, typ)
if isOutput {
g.Fgenf(w,
"%.v.ApplyT(func(x *%[3]s) %[2]s { return %[2]s(*x) }).(%[2]sOutput)",
from, enumTag, underlyingType)
return
}
diag := pcl.GenEnum(to, from, g.genSafeEnum(w, to), func(from model.Expression) {
g.Fgenf(w, "%s(%v)", enumTag, from)
})
if diag != nil {
g.diagnostics = append(g.diagnostics, diag)
}
return
}
switch arg := from.(type) {
2020-05-21 17:23:33 +00:00
case *model.TupleConsExpression:
g.genTupleConsExpression(w, arg, expr.Type())
case *model.ObjectConsExpression:
isInput := false
g.genObjectConsExpression(w, arg, expr.Type(), isInput)
case *model.LiteralValueExpression:
g.genLiteralValueExpression(w, arg, expr.Type())
case *model.TemplateExpression:
g.genTemplateExpression(w, arg, expr.Type())
case *model.ScopeTraversalExpression:
g.genScopeTraversalExpression(w, arg, expr.Type())
2020-05-21 17:23:33 +00:00
default:
g.Fgenf(w, "%.v", expr.Args[0])
2020-05-21 17:23:33 +00:00
}
case pcl.IntrinsicApply:
g.genApply(w, expr)
2020-05-21 17:23:33 +00:00
case "element":
g.genNYI(w, "element")
case "entries":
g.genNYI(w, "call %v", expr.Name)
// 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)", expr.Args[0])
// case *model.MapType, *model.ObjectType:
// g.genNYI(w, "MapOrObjectEntries")
// }
// g.Fgenf(w, " => new { Key = k, Value = v })")
case "fileArchive":
g.Fgenf(w, "pulumi.NewFileArchive(%.v)", expr.Args[0])
case "remoteArchive":
g.Fgenf(w, "pulumi.NewRemoteArchive(%.v)", expr.Args[0])
case "assetArchive":
g.Fgenf(w, "pulumi.NewAssetArchive(%.v)", expr.Args[0])
2020-05-21 17:23:33 +00:00
case "fileAsset":
g.Fgenf(w, "pulumi.NewFileAsset(%.v)", expr.Args[0])
case "stringAsset":
g.Fgenf(w, "pulumi.NewStringAsset(%.v)", expr.Args[0])
case "remoteAsset":
g.Fgenf(w, "pulumi.NewRemoteAsset(%.v)", expr.Args[0])
case "filebase64":
// Assuming the existence of the following helper method
g.Fgenf(w, "filebase64OrPanic(%v)", expr.Args[0])
case "filebase64sha256":
// Assuming the existence of the following helper method
g.Fgenf(w, "filebase64sha256OrPanic(%v)", expr.Args[0])
case "notImplemented":
g.Fgenf(w, "notImplemented(%v)", expr.Args[0])
case "singleOrNone":
g.Fgenf(w, "singleOrNone(%v)", expr.Args[0])
case pcl.Invoke:
if expr.Signature.MultiArgumentInputs {
panic(fmt.Errorf("go program-gen does not implement MultiArgumentInputs for function '%v'",
expr.Args[0]))
}
pkg, module, fn, diags := g.functionName(expr.Args[0])
contract.Assertf(len(diags) == 0, "We don't allow problems getting the function name")
if module == "" || module == "index" {
module = pkg
}
isOut, outArgs, outArgsType := pcl.RecognizeOutputVersionedInvoke(expr)
if isOut {
outTypeName, err := outputVersionFunctionArgTypeName(outArgsType, g.externalCache)
if err != nil {
// We create a diag instead of panicking since panics are caught in go
// format expressions.
g.diagnostics = append(g.diagnostics, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Error when generating an output-versioned Invoke",
Detail: fmt.Sprintf("underlying error: %v", err),
Subject: &hcl.Range{},
Context: &hcl.Range{},
Expression: nil,
EvalContext: &hcl.EvalContext{},
})
g.Fgenf(w, "%q", "failed") // Write a value to avoid syntax errors
return
}
g.Fgenf(w, "%s.%sOutput(ctx, ", module, fn)
g.genObjectConsExpressionWithTypeName(w, outArgs, outArgsType, outTypeName)
} else {
g.Fgenf(w, "%s.%s(ctx, ", module, fn)
g.Fgenf(w, "%.v", expr.Args[1])
}
optionsBag := ""
var buf bytes.Buffer
if len(expr.Args) == 3 {
g.Fgenf(&buf, ", %.v", expr.Args[2])
} else {
g.Fgenf(&buf, ", nil")
}
optionsBag = buf.String()
g.Fgenf(w, "%v)", optionsBag)
case "join":
g.Fgenf(w, "strings.Join(%v, %v)", expr.Args[1], expr.Args[0])
2020-05-21 17:23:33 +00:00
case "length":
g.Fgenf(w, "len(%.20v)", expr.Args[0])
2020-05-21 17:23:33 +00:00
case "lookup":
g.genNYI(w, "Lookup")
case keywordRange:
2020-05-21 17:23:33 +00:00
g.genNYI(w, "call %v", expr.Name)
// g.genRange(w, expr, false)
case "readFile":
// Assuming the existence of the following helper method located earlier in the preamble
g.Fgenf(w, "readFileOrPanic(%v)", expr.Args[0])
2020-05-21 17:23:33 +00:00
case "readDir":
contract.Failf("unlowered readDir function expression @ %v", expr.SyntaxNode().Range())
case "secret":
outputTypeName := "pulumi.Any"
if model.ResolveOutputs(expr.Type()) != model.DynamicType {
outputTypeName = g.argumentTypeName(nil, expr.Type(), false)
}
g.Fgenf(w, "pulumi.ToSecret(%v).(%sOutput)", expr.Args[0], outputTypeName)
2023-01-31 12:31:00 +00:00
case "unsecret":
outputTypeName := "pulumi.Any"
if model.ResolveOutputs(expr.Type()) != model.DynamicType {
outputTypeName = g.argumentTypeName(nil, expr.Type(), false)
}
g.Fgenf(w, "pulumi.Unsecret(%v).(%sOutput)", expr.Args[0], outputTypeName)
2020-05-21 17:23:33 +00:00
case "split":
g.genNYI(w, "call %v", expr.Name)
// g.Fgenf(w, "%.20v.Split(%v)", expr.Args[1], expr.Args[0])
case "toBase64":
g.Fgenf(w, "base64.StdEncoding.EncodeToString([]byte(%v))", expr.Args[0])
2022-09-16 23:12:29 +00:00
case fromBase64Fn:
g.Fgenf(w, "base64.StdEncoding.DecodeString(%v)", expr.Args[0])
2020-05-21 17:23:33 +00:00
case "toJSON":
contract.Failf("unlowered toJSON function expression @ %v", expr.SyntaxNode().Range())
case "mimeType":
g.Fgenf(w, "mime.TypeByExtension(path.Ext(%.v))", expr.Args[0])
case "sha1":
g.Fgenf(w, "sha1Hash(%v)", expr.Args[0])
case "goOptionalFloat64":
g.Fgenf(w, "pulumi.Float64Ref(%.v)", expr.Args[0])
case "goOptionalBool":
g.Fgenf(w, "pulumi.BoolRef(%.v)", expr.Args[0])
case "goOptionalInt":
g.Fgenf(w, "pulumi.IntRef(%.v)", expr.Args[0])
case "goOptionalString":
g.Fgenf(w, "pulumi.StringRef(%.v)", expr.Args[0])
case "stack":
g.Fgen(w, "ctx.Stack()")
case "project":
g.Fgen(w, "ctx.Project()")
case "cwd":
g.Fgen(w, "func(cwd string, err error) string { if err != nil { panic(err) }; return cwd }(os.Getwd())")
2020-05-21 17:23:33 +00:00
default:
g.genNYI(w, "call %v", expr.Name)
}
}
// Currently args type for output-versioned invokes are named
// `FOutputArgs`, but this is not yet understood by `tokenToType`. Use
// this function to compensate.
func outputVersionFunctionArgTypeName(t model.Type, cache *Cache) (string, error) {
schemaType, ok := pcl.GetSchemaForType(t)
if !ok {
return "", fmt.Errorf("No schema.Type type found for the given model.Type")
}
objType, ok := schemaType.(*schema.ObjectType)
if !ok {
return "", fmt.Errorf("Expected a schema.ObjectType, got %s", schemaType.String())
}
pkg := &pkgContext{
pkg: (&schema.Package{Name: "main"}).Reference(),
externalPackages: cache,
}
var ty string
if pkg.isExternalReference(objType) {
extPkg, _ := pkg.contextForExternalReference(objType)
ty = extPkg.tokenToType(objType.Token)
} else {
ty = pkg.tokenToType(objType.Token)
}
return fmt.Sprintf("%sOutputArgs", strings.TrimSuffix(ty, "Args")), nil
}
func (g *generator) GenIndexExpression(w io.Writer, expr *model.IndexExpression) {
g.Fgenf(w, "%.20v[%.v]", expr.Collection, expr.Key)
}
2020-05-21 17:23:33 +00:00
func (g *generator) GenLiteralValueExpression(w io.Writer, expr *model.LiteralValueExpression) {
g.genLiteralValueExpression(w, expr, expr.Type())
}
func (g *generator) genLiteralValueExpression(w io.Writer, expr *model.LiteralValueExpression, destType model.Type) {
exprType := expr.Type()
if cns, ok := exprType.(*model.ConstType); ok {
exprType = cns.Type
}
if exprType == model.NoneType {
g.Fgen(w, "nil")
return
}
argTypeName := g.argumentTypeName(expr, destType, false)
isPulumiType := strings.HasPrefix(argTypeName, "pulumi.")
switch exprType {
case model.BoolType:
if isPulumiType {
g.Fgenf(w, "%s(%v)", argTypeName, expr.Value.True())
} else {
g.Fgenf(w, "%v", expr.Value.True())
}
case model.NumberType, model.IntType:
bf := expr.Value.AsBigFloat()
if i, acc := bf.Int64(); acc == big.Exact {
if isPulumiType {
g.Fgenf(w, "%s(%d)", argTypeName, i)
} else {
g.Fgenf(w, "%d", i)
}
} else {
f, _ := bf.Float64()
if isPulumiType {
g.Fgenf(w, "%s(%g)", argTypeName, f)
} else {
g.Fgenf(w, "%g", f)
}
}
case model.StringType:
strVal := expr.Value.AsString()
if isPulumiType {
g.Fgenf(w, "%s(", argTypeName)
g.genStringLiteral(w, strVal, true /* allow raw */)
g.Fgenf(w, ")")
} else {
g.genStringLiteral(w, strVal, true /* allow raw */)
2020-05-21 17:23:33 +00:00
}
default:
contract.Failf("unexpected opaque type in GenLiteralValueExpression: %v (%v)", destType,
2020-05-21 17:23:33 +00:00
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 {
g.genObjectConsExpressionWithTypeName(w, expr, expr.Type(), configMetadata.TypeName)
return
}
}
}
isInput := false
g.genObjectConsExpression(w, expr, expr.Type(), isInput)
2020-05-21 17:23:33 +00:00
}
func (g *generator) genObjectConsExpression(
w io.Writer,
expr *model.ObjectConsExpression,
destType model.Type,
all: Reformat with gofumpt Per team discussion, switching to gofumpt. [gofumpt][1] is an alternative, stricter alternative to gofmt. It addresses other stylistic concerns that gofmt doesn't yet cover. [1]: https://github.com/mvdan/gofumpt See the full list of [Added rules][2], but it includes: - Dropping empty lines around function bodies - Dropping unnecessary variable grouping when there's only one variable - Ensuring an empty line between multi-line functions - simplification (`-s` in gofmt) is always enabled - Ensuring multi-line function signatures end with `) {` on a separate line. [2]: https://github.com/mvdan/gofumpt#Added-rules gofumpt is stricter, but there's no lock-in. All gofumpt output is valid gofmt output, so if we decide we don't like it, it's easy to switch back without any code changes. gofumpt support is built into the tooling we use for development so this won't change development workflows. - golangci-lint includes a gofumpt check (enabled in this PR) - gopls, the LSP for Go, includes a gofumpt option (see [installation instrutions][3]) [3]: https://github.com/mvdan/gofumpt#installation This change was generated by running: ```bash gofumpt -w $(rg --files -g '*.go' | rg -v testdata | rg -v compilation_error) ``` The following files were manually tweaked afterwards: - pkg/cmd/pulumi/stack_change_secrets_provider.go: one of the lines overflowed and had comments in an inconvenient place - pkg/cmd/pulumi/destroy.go: `var x T = y` where `T` wasn't necessary - pkg/cmd/pulumi/policy_new.go: long line because of error message - pkg/backend/snapshot_test.go: long line trying to assign three variables in the same assignment I have included mention of gofumpt in the CONTRIBUTING.md.
2023-03-03 16:36:39 +00:00
isInput bool,
) {
isInput = isInput || isInputty(destType)
typeName := g.argumentTypeName(expr, destType, isInput)
if schemaType, ok := pcl.GetSchemaForType(destType); ok {
if obj, ok := codegen.UnwrapType(schemaType).(*schema.ObjectType); ok {
if g.useLookupInvokeForm(obj.Token) {
typeName = strings.Replace(typeName, ".Get", ".Lookup", 1)
}
}
}
g.genObjectConsExpressionWithTypeName(w, expr, destType, typeName)
}
func (g *generator) genObjectConsExpressionWithTypeName(
w io.Writer,
expr *model.ObjectConsExpression,
destType model.Type,
all: Reformat with gofumpt Per team discussion, switching to gofumpt. [gofumpt][1] is an alternative, stricter alternative to gofmt. It addresses other stylistic concerns that gofmt doesn't yet cover. [1]: https://github.com/mvdan/gofumpt See the full list of [Added rules][2], but it includes: - Dropping empty lines around function bodies - Dropping unnecessary variable grouping when there's only one variable - Ensuring an empty line between multi-line functions - simplification (`-s` in gofmt) is always enabled - Ensuring multi-line function signatures end with `) {` on a separate line. [2]: https://github.com/mvdan/gofumpt#Added-rules gofumpt is stricter, but there's no lock-in. All gofumpt output is valid gofmt output, so if we decide we don't like it, it's easy to switch back without any code changes. gofumpt support is built into the tooling we use for development so this won't change development workflows. - golangci-lint includes a gofumpt check (enabled in this PR) - gopls, the LSP for Go, includes a gofumpt option (see [installation instrutions][3]) [3]: https://github.com/mvdan/gofumpt#installation This change was generated by running: ```bash gofumpt -w $(rg --files -g '*.go' | rg -v testdata | rg -v compilation_error) ``` The following files were manually tweaked afterwards: - pkg/cmd/pulumi/stack_change_secrets_provider.go: one of the lines overflowed and had comments in an inconvenient place - pkg/cmd/pulumi/destroy.go: `var x T = y` where `T` wasn't necessary - pkg/cmd/pulumi/policy_new.go: long line because of error message - pkg/backend/snapshot_test.go: long line trying to assign three variables in the same assignment I have included mention of gofumpt in the CONTRIBUTING.md.
2023-03-03 16:36:39 +00:00
typeName string,
) {
if len(expr.Items) == 0 {
g.Fgenf(w, "nil")
return
}
var temps []interface{}
// TODO: @pgavlin --- ineffectual assignment, was there some work in flight here?
// if strings.HasSuffix(typeName, "Args") {
// isInput = true
// }
// // invokes are not inputty
// if strings.Contains(typeName, ".Lookup") || strings.Contains(typeName, ".Get") {
// isInput = false
// }
isMap := strings.HasPrefix(typeName, "map[")
// TODO: retrieve schema and propagate optionals to emit bool ptr, etc.
// first lower all inner expressions and emit temps
for i, item := range expr.Items {
// don't treat keys as inputs
sdk/go: Remove 'nolint' directives from package docs Go treats comments that match the following regex as directives. //[a-z0-9]+:[a-z0-9] Comments that are directives don't show in an entity's documentation. https://github.com/golang/go/commit/5a550b695117f07a4f2454039a4871250cd3ed09#diff-f56160fd9fcea272966a8a1d692ad9f49206fdd8dbcbfe384865a98cd9bc2749R165 Our code has `//nolint` directives that now show in the API Reference. This is because these directives are in one of the following forms, which don't get this special treatment. // nolint:foo //nolint: foo This change fixes all such directives found by the regex: `// nolint|//nolint: `. See bottom of commit for command used for the fix. Verification: Here's the output of `go doc` on some entities before and after this change. Before ``` % go doc github.com/pulumi/pulumi/sdk/v3/go/pulumi | head -n8 package pulumi // import "github.com/pulumi/pulumi/sdk/v3/go/pulumi" nolint: lll, interfacer nolint: lll, interfacer const EnvOrganization = "PULUMI_ORGANIZATION" ... var ErrPlugins = errors.New("pulumi: plugins requested") ``` After ``` % go doc github.com/pulumi/pulumi/sdk/v3/go/pulumi | head -n8 package pulumi // import "github.com/pulumi/pulumi/sdk/v3/go/pulumi" const EnvOrganization = "PULUMI_ORGANIZATION" ... var ErrPlugins = errors.New("pulumi: plugins requested") func BoolRef(v bool) *bool func Float64Ref(v float64) *float64 func IntRef(v int) *int func IsSecret(o Output) bool ``` Before ``` % go doc github.com/pulumi/pulumi/sdk/v3/go/pulumi URN_ package pulumi // import "github.com/pulumi/pulumi/sdk/v3/go/pulumi" func URN_(o string) ResourceOption URN_ is an optional URN of a previously-registered resource of this type to read from the engine. nolint: revive ``` After: ``` % go doc github.com/pulumi/pulumi/sdk/v3/go/pulumi URN_ package pulumi // import "github.com/pulumi/pulumi/sdk/v3/go/pulumi" func URN_(o string) ResourceOption URN_ is an optional URN of a previously-registered resource of this type to read from the engine. ``` Note that golangci-lint offers a 'nolintlint' linter that finds such miuses of nolint, but it also finds other issues so I've deferred that to a follow up PR. Resolves #11785 Related: https://github.com/golangci/golangci-lint/issues/892 [git-generate] FILES=$(mktemp) rg -l '// nolint|//nolint: ' | tee "$FILES" | xargs perl -p -i -e ' s|// nolint|//nolint|g; s|//nolint: |//nolint:|g; ' rg '.go$' < "$FILES" | xargs gofmt -w -s
2023-01-06 00:07:45 +00:00
//nolint:revive
k, kTemps := g.lowerExpression(item.Key, item.Key.Type())
temps = append(temps, kTemps...)
item.Key = k
x, xTemps := g.lowerExpression(item.Value, item.Value.Type())
[go/program-gen] Fix using inline invoke expressions inside resources, objects and arrays (#14484) # Description This PR fixes an issue in Go program where if users are writing invoke expressions inline inside other expressions, then that invoke is extracted into a temporary variable and then later referenced the same way it was used. This is because non-output-versioned invokes cannot be used directly since they return a tuple (InvokeResult, err) so we need to check for the error before proceeding. Fixes #14064 ## 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. -->
2023-11-03 14:22:43 +00:00
x, invokeTemps := g.rewriteInlineInvokes(x)
temps = append(temps, xTemps...)
[go/program-gen] Fix using inline invoke expressions inside resources, objects and arrays (#14484) # Description This PR fixes an issue in Go program where if users are writing invoke expressions inline inside other expressions, then that invoke is extracted into a temporary variable and then later referenced the same way it was used. This is because non-output-versioned invokes cannot be used directly since they return a tuple (InvokeResult, err) so we need to check for the error before proceeding. Fixes #14064 ## 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. -->
2023-11-03 14:22:43 +00:00
for _, t := range invokeTemps {
temps = append(temps, t)
}
item.Value = x
expr.Items[i] = item
}
g.genTemps(w, temps)
if g.inGenTupleConExprListArgs {
if g.isPtrArg {
g.Fgenf(w, "&%s", typeName)
}
} else if isMap || !strings.HasSuffix(typeName, "Args") || strings.HasSuffix(typeName, "OutputArgs") {
g.Fgenf(w, "%s", typeName)
} else {
g.Fgenf(w, "&%s", typeName)
}
g.Fgenf(w, "{\n")
for _, item := range expr.Items {
if lit, ok := g.literalKey(item.Key); ok {
if isMap || strings.HasSuffix(typeName, "Map") {
g.Fgenf(w, "\"%s\"", lit)
} else {
g.Fgenf(w, "%s", Title(lit))
2020-05-21 17:23:33 +00:00
}
} else {
g.Fgenf(w, "%.v", item.Key)
2020-05-21 17:23:33 +00:00
}
g.Fgenf(w, ": %.v,\n", item.Value)
}
g.Fgenf(w, "}")
}
2020-05-21 17:23:33 +00:00
func (g *generator) GenRelativeTraversalExpression(w io.Writer, expr *model.RelativeTraversalExpression) {
g.Fgenf(w, "%.20v", expr.Source)
isRootResource := false
if ie, ok := expr.Source.(*model.IndexExpression); ok {
if se, ok := ie.Collection.(*model.ScopeTraversalExpression); ok {
if _, ok := se.Parts[0].(*pcl.Resource); ok {
isRootResource = true
}
}
}
g.genRelativeTraversal(w, expr.Traversal, expr.Parts, isRootResource)
2020-05-21 17:23:33 +00:00
}
func (g *generator) GenScopeTraversalExpression(w io.Writer, expr *model.ScopeTraversalExpression) {
g.genScopeTraversalExpression(w, expr, expr.Type())
}
func (g *generator) genScopeTraversalExpression(
all: Reformat with gofumpt Per team discussion, switching to gofumpt. [gofumpt][1] is an alternative, stricter alternative to gofmt. It addresses other stylistic concerns that gofmt doesn't yet cover. [1]: https://github.com/mvdan/gofumpt See the full list of [Added rules][2], but it includes: - Dropping empty lines around function bodies - Dropping unnecessary variable grouping when there's only one variable - Ensuring an empty line between multi-line functions - simplification (`-s` in gofmt) is always enabled - Ensuring multi-line function signatures end with `) {` on a separate line. [2]: https://github.com/mvdan/gofumpt#Added-rules gofumpt is stricter, but there's no lock-in. All gofumpt output is valid gofmt output, so if we decide we don't like it, it's easy to switch back without any code changes. gofumpt support is built into the tooling we use for development so this won't change development workflows. - golangci-lint includes a gofumpt check (enabled in this PR) - gopls, the LSP for Go, includes a gofumpt option (see [installation instrutions][3]) [3]: https://github.com/mvdan/gofumpt#installation This change was generated by running: ```bash gofumpt -w $(rg --files -g '*.go' | rg -v testdata | rg -v compilation_error) ``` The following files were manually tweaked afterwards: - pkg/cmd/pulumi/stack_change_secrets_provider.go: one of the lines overflowed and had comments in an inconvenient place - pkg/cmd/pulumi/destroy.go: `var x T = y` where `T` wasn't necessary - pkg/cmd/pulumi/policy_new.go: long line because of error message - pkg/backend/snapshot_test.go: long line trying to assign three variables in the same assignment I have included mention of gofumpt in the CONTRIBUTING.md.
2023-03-03 16:36:39 +00:00
w io.Writer, expr *model.ScopeTraversalExpression, destType model.Type,
) {
2020-05-21 17:23:33 +00:00
rootName := expr.RootName
if _, ok := expr.Parts[0].(*model.SplatVariable); ok {
rootName = "val0"
}
2020-05-21 17:23:33 +00:00
genIDCall := false
2020-05-21 17:23:33 +00:00
isInput := false
if schemaType, ok := pcl.GetSchemaForType(destType); ok {
_, isInput = schemaType.(*schema.InputType)
}
var sourceIsPlain bool
switch root := expr.Parts[0].(type) {
case *pcl.Resource:
isInput = false
if _, ok := pcl.GetSchemaForType(root.InputType); ok {
// convert .id into .ID()
last := expr.Traversal[len(expr.Traversal)-1]
if attr, ok := last.(hcl.TraverseAttr); ok && attr.Name == "id" {
genIDCall = true
expr.Traversal = expr.Traversal[:len(expr.Traversal)-1]
}
2020-05-21 17:23:33 +00:00
}
case *pcl.LocalVariable:
if root, ok := root.Definition.Value.(*model.FunctionCallExpression); ok && !pcl.IsOutputVersionInvokeCall(root) {
sourceIsPlain = true
}
case *pcl.ConfigVariable:
if g.isComponent {
// config variables of components are always of type Input<T>
// these shouldn't be wrapped in a pulumi.String(...), pulumi.Int(...) etc. functions
g.Fgenf(w, "args.%s", Title(rootName))
isRootResource := false
g.genRelativeTraversal(w, expr.Traversal.SimpleSplit().Rel, expr.Parts[1:], isRootResource)
return
}
2020-05-21 17:23:33 +00:00
}
// TODO if it's an array type, we need a lowering step to turn []string -> pulumi.StringArray
if isInput {
argTypeName := g.argumentTypeName(expr, expr.Type(), isInput)
if strings.HasSuffix(argTypeName, "Array") {
destTypeName := g.argumentTypeName(expr, destType, isInput)
// `argTypeName` == `destTypeName` and `argTypeName` ends with `Array`, we
// know that `destType` is an outputty type. If the source is plain (and thus
// not outputty), then the types can never line up and we will need a
// conversion helper method.
if argTypeName != destTypeName || sourceIsPlain {
// use a helper to transform prompt arrays into inputty arrays
var helper *promptToInputArrayHelper
if h, ok := g.arrayHelpers[argTypeName]; ok {
helper = h
} else {
// helpers are emitted at the end in the postamble step
helper = &promptToInputArrayHelper{
destType: argTypeName,
}
g.arrayHelpers[argTypeName] = helper
}
// Wrap the emitted expression in a call to the generated helper function.
g.Fgenf(w, "%s(", helper.getFnName())
defer g.Fgenf(w, ")")
}
} else {
// Wrap the emitted expression in a type conversion.
g.Fgenf(w, "%s(", g.argumentTypeName(expr, expr.Type(), isInput))
defer g.Fgenf(w, ")")
}
}
// TODO: this isn't exhaustively correct as "range" could be a legit var name
// instead we should probably use a fn call expression here for entries/range
// similar to other languages
if rootName == keywordRange {
part := expr.Traversal[1].(hcl.TraverseAttr).Name
switch part {
case "value":
g.Fgenf(w, "val0")
case "key":
g.Fgenf(w, "key0")
default:
contract.Failf("unexpected traversal on range expression: %s", part)
}
} else {
g.Fgen(w, makeValidIdentifier(rootName))
isRootResource := false
g.genRelativeTraversal(w, expr.Traversal.SimpleSplit().Rel, expr.Parts[1:], isRootResource)
}
if genIDCall {
g.Fgenf(w, ".ID()")
}
2020-05-21 17:23:33 +00:00
}
// GenSplatExpression generates code for a SplatExpression.
func (g *generator) GenSplatExpression(w io.Writer, expr *model.SplatExpression) {
contract.Failf("unlowered splat expression @ %v", expr.SyntaxNode().Range())
}
2020-05-21 17:23:33 +00:00
// GenTemplateExpression generates code for a TemplateExpression.
func (g *generator) GenTemplateExpression(w io.Writer, expr *model.TemplateExpression) {
g.genTemplateExpression(w, expr, expr.Type())
}
func (g *generator) genTemplateExpression(w io.Writer, expr *model.TemplateExpression, destType model.Type) {
if len(expr.Parts) == 1 {
if lit, ok := expr.Parts[0].(*model.LiteralValueExpression); ok && model.StringType.AssignableFrom(lit.Type()) {
g.genLiteralValueExpression(w, lit, destType)
}
// If we have a template expression that doesn't start with a string, it indicates
// an invalid *pcl.Program. Instead of crashing, we continue.
return
}
argTypeName := g.argumentTypeName(expr, destType, false)
isPulumiType := strings.HasPrefix(argTypeName, "pulumi.")
if isPulumiType {
g.Fgenf(w, "%s(", argTypeName)
defer g.Fgenf(w, ")")
}
var fmtStr strings.Builder
args := new(bytes.Buffer)
canBeRaw := true
for _, v := range expr.Parts {
if lit, ok := v.(*model.LiteralValueExpression); ok && lit.Value.Type().Equals(cty.String) {
str := lit.Value.AsString()
// We don't want to accidentally embed a formatting directive in our
// formatting string.
if !strings.ContainsRune(str, '%') {
if canBeRaw && strings.ContainsRune(str, '`') {
canBeRaw = false
}
// Build the formatting string
fmtStr.WriteString(str)
continue
}
}
// v cannot be directly inserted into the formatting string, so put it in the
// argument list.
fmtStr.WriteString("%v")
g.Fgenf(args, ", %.v", v)
}
g.Fgenf(w, "fmt.Sprintf(")
g.genStringLiteral(w, fmtStr.String(), canBeRaw)
_, err := args.WriteTo(w)
contract.AssertNoErrorf(err, "Failed to write arguments")
g.Fgenf(w, ")")
}
2020-05-21 17:23:33 +00:00
// GenTemplateJoinExpression generates code for a TemplateJoinExpression.
func (g *generator) GenTemplateJoinExpression(w io.Writer, expr *model.TemplateJoinExpression) { /*TODO*/
}
func (g *generator) GenTupleConsExpression(w io.Writer, expr *model.TupleConsExpression) {
g.genTupleConsExpression(w, expr, expr.Type())
}
// GenTupleConsExpression generates code for a TupleConsExpression.
func (g *generator) genTupleConsExpression(w io.Writer, expr *model.TupleConsExpression, destType model.Type) {
isInput := isInputty(destType)
var temps []interface{}
for i, item := range expr.Expressions {
item, itemTemps := g.lowerExpression(item, item.Type())
[go/program-gen] Fix using inline invoke expressions inside resources, objects and arrays (#14484) # Description This PR fixes an issue in Go program where if users are writing invoke expressions inline inside other expressions, then that invoke is extracted into a temporary variable and then later referenced the same way it was used. This is because non-output-versioned invokes cannot be used directly since they return a tuple (InvokeResult, err) so we need to check for the error before proceeding. Fixes #14064 ## 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. -->
2023-11-03 14:22:43 +00:00
item, invokeTemps := g.rewriteInlineInvokes(item)
temps = append(temps, itemTemps...)
[go/program-gen] Fix using inline invoke expressions inside resources, objects and arrays (#14484) # Description This PR fixes an issue in Go program where if users are writing invoke expressions inline inside other expressions, then that invoke is extracted into a temporary variable and then later referenced the same way it was used. This is because non-output-versioned invokes cannot be used directly since they return a tuple (InvokeResult, err) so we need to check for the error before proceeding. Fixes #14064 ## 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. -->
2023-11-03 14:22:43 +00:00
for _, t := range invokeTemps {
temps = append(temps, t)
}
expr.Expressions[i] = item
}
g.genTemps(w, temps)
argType := g.argumentTypeName(expr, destType, isInput)
// don't need to generate type for list args if not a pointer, i.e. []ec2.SubnetSpecArgs{ {Type: ...} }
// unless it contains an interface, i.e. []map[string]interface{ map[string]interface{"key": "val"} }
if strings.HasPrefix(argType, "[]") && !strings.Contains(argType, "interface{}") {
defer func(b bool) { g.inGenTupleConExprListArgs = b }(g.inGenTupleConExprListArgs)
g.inGenTupleConExprListArgs = true
if strings.HasPrefix(argType, "[]*") {
defer func(b bool) { g.isPtrArg = b }(g.isPtrArg)
g.isPtrArg = true
}
}
2020-05-21 17:23:33 +00:00
g.Fgenf(w, "%s{\n", argType)
switch len(expr.Expressions) {
case 0:
// empty array
break
default:
for _, v := range expr.Expressions {
g.Fgenf(w, "%v,\n", v)
}
}
g.Fgenf(w, "}")
}
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)
}
2020-05-21 17:23:33 +00:00
// argumentTypeName computes the go type for the given expression and model type.
func (g *generator) argumentTypeName(expr model.Expression, destType model.Type, isInput bool) (result string) {
if cns, ok := destType.(*model.ConstType); ok {
destType = cns.Type
}
// This can happen with null literals.
if destType == model.NoneType {
return ""
}
if schemaType, ok := pcl.GetSchemaForType(destType); ok {
return (&pkgContext{
pkg: (&schema.Package{Name: "main"}).Reference(),
externalPackages: g.externalCache,
}).argsType(schemaType)
2020-05-21 17:23:33 +00:00
}
switch destType := destType.(type) {
case *model.OpaqueType:
switch *destType {
case *model.IntType:
if isInput {
return "pulumi.Int"
}
return "int"
case *model.NumberType:
if isInput {
return "pulumi.Float64"
}
return "float64"
case *model.StringType:
if isInput {
return "pulumi.String"
}
return "string"
case *model.BoolType:
if isInput {
return "pulumi.Bool"
}
return "bool"
case *model.DynamicType:
if isInput {
return "pulumi.Any"
}
return "interface{}"
default:
return string(*destType)
}
case *model.ObjectType:
if isInput {
// check for element type uniformity and return appropriate type if so
allSameType := true
var elmType string
for _, v := range destType.Properties {
valType := g.argumentTypeName(nil, v, true)
if elmType != "" && elmType != valType {
allSameType = false
break
}
elmType = valType
}
if allSameType && elmType != "" {
return fmt.Sprintf("%sMap", elmType)
}
return "pulumi.Map"
}
return "map[string]interface{}"
case *model.MapType:
valType := g.argumentTypeName(nil, destType.ElementType, isInput)
if isInput {
if Title(valType) == "pulumi.Any" {
return "pulumi.Map"
}
return fmt.Sprintf("pulumi.%sMap", Title(valType))
}
return fmt.Sprintf("map[string]%s", valType)
case *model.ListType:
argTypeName := g.argumentTypeName(nil, destType.ElementType, isInput)
if strings.HasPrefix(argTypeName, "pulumi.") && argTypeName != "pulumi.Resource" {
if argTypeName == "pulumi.Any" {
return "pulumi.Array"
}
return fmt.Sprintf("%sArray", argTypeName)
}
return fmt.Sprintf("[]%s", argTypeName)
case *model.TupleType:
// attempt to collapse tuple types. intentionally does not use model.UnifyTypes
// correct go code requires all types to match, or use of interface{}
var elmType model.Type
for i, t := range destType.ElementTypes {
if i == 0 {
elmType = t
if cns, ok := elmType.(*model.ConstType); ok {
elmType = cns.Type
}
continue
}
if !elmType.AssignableFrom(t) {
elmType = nil
break
}
}
if elmType != nil {
argTypeName := g.argumentTypeName(nil, elmType, isInput)
if strings.HasPrefix(argTypeName, "pulumi.") && argTypeName != "pulumi.Resource" {
if argTypeName == "pulumi.Any" {
return "pulumi.Array"
}
return fmt.Sprintf("%sArray", argTypeName)
}
return fmt.Sprintf("[]%s", argTypeName)
}
if isInput {
return "pulumi.Array"
}
return "[]interface{}"
case *model.OutputType:
isInput = true
return g.argumentTypeName(expr, destType.ElementType, isInput)
case *model.UnionType:
for _, ut := range destType.ElementTypes {
isOptional := false
// check if the union contains none, which indicates this is an optional value
for _, ut := range destType.ElementTypes {
if ut.Equals(model.NoneType) {
isOptional = true
}
}
switch ut := ut.(type) {
case *model.OpaqueType:
if isOptional {
return g.argumentTypeNamePtr(expr, ut, isInput)
}
return g.argumentTypeName(expr, ut, isInput)
case *model.ConstType:
return g.argumentTypeName(expr, ut.Type, isInput)
case *model.TupleType:
return g.argumentTypeName(expr, ut, isInput)
}
}
return "interface{}"
case *model.PromiseType:
return g.argumentTypeName(expr, destType.ElementType, isInput)
default:
contract.Failf("unexpected destType type %T", destType)
}
2020-05-21 17:23:33 +00:00
return ""
}
func (g *generator) argumentTypeNamePtr(expr model.Expression, destType model.Type, isInput bool) (result string) {
res := g.argumentTypeName(expr, destType, isInput)
return "*" + res
}
2020-05-21 17:23:33 +00:00
func (g *generator) genRelativeTraversal(w io.Writer,
all: Reformat with gofumpt Per team discussion, switching to gofumpt. [gofumpt][1] is an alternative, stricter alternative to gofmt. It addresses other stylistic concerns that gofmt doesn't yet cover. [1]: https://github.com/mvdan/gofumpt See the full list of [Added rules][2], but it includes: - Dropping empty lines around function bodies - Dropping unnecessary variable grouping when there's only one variable - Ensuring an empty line between multi-line functions - simplification (`-s` in gofmt) is always enabled - Ensuring multi-line function signatures end with `) {` on a separate line. [2]: https://github.com/mvdan/gofumpt#Added-rules gofumpt is stricter, but there's no lock-in. All gofumpt output is valid gofmt output, so if we decide we don't like it, it's easy to switch back without any code changes. gofumpt support is built into the tooling we use for development so this won't change development workflows. - golangci-lint includes a gofumpt check (enabled in this PR) - gopls, the LSP for Go, includes a gofumpt option (see [installation instrutions][3]) [3]: https://github.com/mvdan/gofumpt#installation This change was generated by running: ```bash gofumpt -w $(rg --files -g '*.go' | rg -v testdata | rg -v compilation_error) ``` The following files were manually tweaked afterwards: - pkg/cmd/pulumi/stack_change_secrets_provider.go: one of the lines overflowed and had comments in an inconvenient place - pkg/cmd/pulumi/destroy.go: `var x T = y` where `T` wasn't necessary - pkg/cmd/pulumi/policy_new.go: long line because of error message - pkg/backend/snapshot_test.go: long line trying to assign three variables in the same assignment I have included mention of gofumpt in the CONTRIBUTING.md.
2023-03-03 16:36:39 +00:00
traversal hcl.Traversal, parts []model.Traversable, isRootResource bool,
) {
for i, part := range traversal {
2020-05-21 17:23:33 +00:00
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())
}
// TODO handle optionals in go
// if model.IsOptionalType(model.GetTraversableType(parts[i])) {
// g.Fgen(w, "?")
// }
switch key.Type() {
case cty.String:
shouldConvert := isRootResource
if _, ok := parts[i].(*model.OutputType); ok {
shouldConvert = true
}
if key.AsString() == "id" && shouldConvert {
g.Fgenf(w, ".ID()")
} else {
g.Fgenf(w, ".%s", Title(key.AsString()))
}
2020-05-21 17:23:33 +00:00
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())
}
}
}
type nameInfo int
func (nameInfo) Format(name string) string {
// TODO
return name
}
// lowerExpression amends the expression with intrinsics for Go generation.
func (g *generator) lowerExpression(expr model.Expression, typ model.Type) (
all: Reformat with gofumpt Per team discussion, switching to gofumpt. [gofumpt][1] is an alternative, stricter alternative to gofmt. It addresses other stylistic concerns that gofmt doesn't yet cover. [1]: https://github.com/mvdan/gofumpt See the full list of [Added rules][2], but it includes: - Dropping empty lines around function bodies - Dropping unnecessary variable grouping when there's only one variable - Ensuring an empty line between multi-line functions - simplification (`-s` in gofmt) is always enabled - Ensuring multi-line function signatures end with `) {` on a separate line. [2]: https://github.com/mvdan/gofumpt#Added-rules gofumpt is stricter, but there's no lock-in. All gofumpt output is valid gofmt output, so if we decide we don't like it, it's easy to switch back without any code changes. gofumpt support is built into the tooling we use for development so this won't change development workflows. - golangci-lint includes a gofumpt check (enabled in this PR) - gopls, the LSP for Go, includes a gofumpt option (see [installation instrutions][3]) [3]: https://github.com/mvdan/gofumpt#installation This change was generated by running: ```bash gofumpt -w $(rg --files -g '*.go' | rg -v testdata | rg -v compilation_error) ``` The following files were manually tweaked afterwards: - pkg/cmd/pulumi/stack_change_secrets_provider.go: one of the lines overflowed and had comments in an inconvenient place - pkg/cmd/pulumi/destroy.go: `var x T = y` where `T` wasn't necessary - pkg/cmd/pulumi/policy_new.go: long line because of error message - pkg/backend/snapshot_test.go: long line trying to assign three variables in the same assignment I have included mention of gofumpt in the CONTRIBUTING.md.
2023-03-03 16:36:39 +00:00
model.Expression, []interface{},
) {
expr = pcl.RewritePropertyReferences(expr)
expr, diags := pcl.RewriteApplies(expr, nameInfo(0), false /*TODO*/)
expr, sTemps, splatDiags := g.rewriteSplat(expr, g.splatSpiller)
[go/program-gen] Fix using inline invoke expressions inside resources, objects and arrays (#14484) # Description This PR fixes an issue in Go program where if users are writing invoke expressions inline inside other expressions, then that invoke is extracted into a temporary variable and then later referenced the same way it was used. This is because non-output-versioned invokes cannot be used directly since they return a tuple (InvokeResult, err) so we need to check for the error before proceeding. Fixes #14064 ## 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. -->
2023-11-03 14:22:43 +00:00
expr, convertDiags := pcl.RewriteConversions(expr, typ)
expr, tTemps, ternDiags := g.rewriteTernaries(expr, g.ternaryTempSpiller)
expr, jTemps, jsonDiags := g.rewriteToJSON(expr)
expr, rTemps, readDirDiags := g.rewriteReadDir(expr, g.readDirTempSpiller)
expr, oTemps, optDiags := g.rewriteOptionals(expr, g.optionalSpiller)
all: Reformat with gofumpt Per team discussion, switching to gofumpt. [gofumpt][1] is an alternative, stricter alternative to gofmt. It addresses other stylistic concerns that gofmt doesn't yet cover. [1]: https://github.com/mvdan/gofumpt See the full list of [Added rules][2], but it includes: - Dropping empty lines around function bodies - Dropping unnecessary variable grouping when there's only one variable - Ensuring an empty line between multi-line functions - simplification (`-s` in gofmt) is always enabled - Ensuring multi-line function signatures end with `) {` on a separate line. [2]: https://github.com/mvdan/gofumpt#Added-rules gofumpt is stricter, but there's no lock-in. All gofumpt output is valid gofmt output, so if we decide we don't like it, it's easy to switch back without any code changes. gofumpt support is built into the tooling we use for development so this won't change development workflows. - golangci-lint includes a gofumpt check (enabled in this PR) - gopls, the LSP for Go, includes a gofumpt option (see [installation instrutions][3]) [3]: https://github.com/mvdan/gofumpt#installation This change was generated by running: ```bash gofumpt -w $(rg --files -g '*.go' | rg -v testdata | rg -v compilation_error) ``` The following files were manually tweaked afterwards: - pkg/cmd/pulumi/stack_change_secrets_provider.go: one of the lines overflowed and had comments in an inconvenient place - pkg/cmd/pulumi/destroy.go: `var x T = y` where `T` wasn't necessary - pkg/cmd/pulumi/policy_new.go: long line because of error message - pkg/backend/snapshot_test.go: long line trying to assign three variables in the same assignment I have included mention of gofumpt in the CONTRIBUTING.md.
2023-03-03 16:36:39 +00:00
bufferSize := len(tTemps) + len(jTemps) + len(rTemps) + len(sTemps) + len(oTemps)
temps := slice.Prealloc[interface{}](bufferSize)
for _, t := range tTemps {
temps = append(temps, t)
}
for _, t := range jTemps {
temps = append(temps, t)
}
for _, t := range rTemps {
temps = append(temps, t)
}
for _, t := range sTemps {
temps = append(temps, t)
}
for _, t := range oTemps {
temps = append(temps, t)
}
diags = append(diags, convertDiags...)
diags = append(diags, ternDiags...)
diags = append(diags, jsonDiags...)
diags = append(diags, readDirDiags...)
diags = append(diags, splatDiags...)
diags = append(diags, optDiags...)
g.diagnostics = g.diagnostics.Extend(diags)
return expr, temps
2020-05-21 17:23:33 +00:00
}
func (g *generator) genNYI(w io.Writer, reason string, vs ...interface{}) {
2023-06-02 15:17:59 +00:00
message := fmt.Sprintf("not yet implemented: %s", fmt.Sprintf(reason, vs...))
g.diagnostics = append(g.diagnostics, &hcl.Diagnostic{
Severity: hcl.DiagWarning,
Summary: message,
Detail: message,
})
2020-05-21 17:23:33 +00:00
g.Fgenf(w, "\"TODO: %s\"", fmt.Sprintf(reason, vs...))
}
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)
isInput := false
retType := g.argumentTypeName(nil, then.Signature.ReturnType, isInput)
// TODO account for outputs in other namespaces like aws
// TODO[pulumi/pulumi#8453] incomplete pattern code below.
var typeAssertion string
if retType == "[]string" {
typeAssertion = ".(pulumi.StringArrayOutput)"
} else {
if strings.HasPrefix(retType, "*") {
retType = Title(strings.TrimPrefix(retType, "*")) + "Ptr"
switch then.Body.(type) {
case *model.ScopeTraversalExpression:
traversal := then.Body.(*model.ScopeTraversalExpression)
traversal.RootName = "&" + traversal.RootName
then.Body = traversal
}
}
typeAssertion = fmt.Sprintf(".(%sOutput)", retType)
if !strings.Contains(retType, ".") {
typeAssertion = fmt.Sprintf(".(pulumi.%sOutput)", Title(retType))
}
}
if len(applyArgs) == 1 {
// If we only have a single output, just generate a normal `.Apply`
g.Fgenf(w, "%.v.ApplyT(%.v)%s", applyArgs[0], then, typeAssertion)
} else {
g.Fgenf(w, "pulumi.All(%.v", applyArgs[0])
applyArgs = applyArgs[1:]
for _, a := range applyArgs {
g.Fgenf(w, ",%.v", a)
}
2020-06-19 00:17:51 +00:00
allApplyThen, typeConvDecls := g.rewriteThenForAllApply(then)
g.Fgenf(w, ").ApplyT(")
g.genAnonymousFunctionExpression(w, allApplyThen, typeConvDecls, true)
2020-06-19 00:17:51 +00:00
g.Fgenf(w, ")%s", typeAssertion)
}
}
2020-06-19 00:17:51 +00:00
// rewriteThenForAllApply rewrites an apply func after a .All replacing params with []interface{}
// other languages like javascript take advantage of destructuring to simplify All.Apply
// by generating something like [a1, a2, a3]
// Go doesn't support this syntax so we create a set of var decls with type assertions
// to prepend to the body: a1 := _args[0].(string) ... etc.
func (g *generator) rewriteThenForAllApply(
then *model.AnonymousFunctionExpression,
) (*model.AnonymousFunctionExpression, []string) {
typeConvDecls := slice.Prealloc[string](len(then.Parameters))
2020-06-19 00:17:51 +00:00
for i, v := range then.Parameters {
typ := g.argumentTypeName(nil, v.VariableType, false)
decl := fmt.Sprintf("%s := _args[%d].(%s)", v.Name, i, typ)
typeConvDecls = append(typeConvDecls, decl)
}
// dummy type that will produce []interface{} for argumentTypeName
interfaceArrayType := &model.TupleType{
ElementTypes: []model.Type{
model.BoolType, model.StringType, model.IntType,
},
}
then.Parameters = []*model.Variable{{
Name: "_args",
VariableType: interfaceArrayType,
}}
then.Signature.Parameters = []model.Parameter{{
Name: "_args",
Type: interfaceArrayType,
}}
return then, typeConvDecls
}
// Writes a Go string literal.
// The literal will be a raw string literal if allowRaw is true
// and the string is long enough to benefit from it.
func (g *generator) genStringLiteral(w io.Writer, v string, allowRaw bool) {
// If the string is longer than 50 characters,
// contains at least 5 newlines,
// and does not contain a backtick,
// use a backtick string literal for readability.
canBeRaw := len(v) > 50 &&
strings.Count(v, "\n") >= 5 &&
!strings.Contains(v, "`")
if allowRaw && canBeRaw {
fmt.Fprintf(w, "`%s`", v)
return
}
g.Fgen(w, "\"")
g.Fgen(w, g.escapeString(v))
g.Fgen(w, "\"")
}
func (g *generator) escapeString(v string) string {
builder := strings.Builder{}
for _, c := range v {
if c == '"' || c == '\\' {
builder.WriteRune('\\')
}
if c == '\n' {
builder.WriteRune('\\')
builder.WriteRune('n')
continue
}
builder.WriteRune(c)
}
return builder.String()
}
sdk/go: Remove 'nolint' directives from package docs Go treats comments that match the following regex as directives. //[a-z0-9]+:[a-z0-9] Comments that are directives don't show in an entity's documentation. https://github.com/golang/go/commit/5a550b695117f07a4f2454039a4871250cd3ed09#diff-f56160fd9fcea272966a8a1d692ad9f49206fdd8dbcbfe384865a98cd9bc2749R165 Our code has `//nolint` directives that now show in the API Reference. This is because these directives are in one of the following forms, which don't get this special treatment. // nolint:foo //nolint: foo This change fixes all such directives found by the regex: `// nolint|//nolint: `. See bottom of commit for command used for the fix. Verification: Here's the output of `go doc` on some entities before and after this change. Before ``` % go doc github.com/pulumi/pulumi/sdk/v3/go/pulumi | head -n8 package pulumi // import "github.com/pulumi/pulumi/sdk/v3/go/pulumi" nolint: lll, interfacer nolint: lll, interfacer const EnvOrganization = "PULUMI_ORGANIZATION" ... var ErrPlugins = errors.New("pulumi: plugins requested") ``` After ``` % go doc github.com/pulumi/pulumi/sdk/v3/go/pulumi | head -n8 package pulumi // import "github.com/pulumi/pulumi/sdk/v3/go/pulumi" const EnvOrganization = "PULUMI_ORGANIZATION" ... var ErrPlugins = errors.New("pulumi: plugins requested") func BoolRef(v bool) *bool func Float64Ref(v float64) *float64 func IntRef(v int) *int func IsSecret(o Output) bool ``` Before ``` % go doc github.com/pulumi/pulumi/sdk/v3/go/pulumi URN_ package pulumi // import "github.com/pulumi/pulumi/sdk/v3/go/pulumi" func URN_(o string) ResourceOption URN_ is an optional URN of a previously-registered resource of this type to read from the engine. nolint: revive ``` After: ``` % go doc github.com/pulumi/pulumi/sdk/v3/go/pulumi URN_ package pulumi // import "github.com/pulumi/pulumi/sdk/v3/go/pulumi" func URN_(o string) ResourceOption URN_ is an optional URN of a previously-registered resource of this type to read from the engine. ``` Note that golangci-lint offers a 'nolintlint' linter that finds such miuses of nolint, but it also finds other issues so I've deferred that to a follow up PR. Resolves #11785 Related: https://github.com/golangci/golangci-lint/issues/892 [git-generate] FILES=$(mktemp) rg -l '// nolint|//nolint: ' | tee "$FILES" | xargs perl -p -i -e ' s|// nolint|//nolint|g; s|//nolint: |//nolint:|g; ' rg '.go$' < "$FILES" | xargs gofmt -w -s
2023-01-06 00:07:45 +00:00
//nolint:lll
func isInputty(destType model.Type) bool {
// TODO this needs to be more robust, likely the inverse of:
// https://github.com/pulumi/pulumi/blob/5330c97684cad78bcc60d8867f1b28704bd8a555/pkg/codegen/hcl2/model/type_eventuals.go#L244
switch destType := destType.(type) {
case *model.UnionType:
for _, t := range destType.ElementTypes {
if _, ok := t.(*model.OutputType); ok {
return true
}
}
case *model.OutputType:
return true
}
return false
}
func (g *generator) literalKey(x model.Expression) (string, bool) {
strKey := ""
switch x := x.(type) {
case *model.LiteralValueExpression:
if model.StringType.AssignableFrom(x.Type()) {
strKey = x.Value.AsString()
break
}
var buf bytes.Buffer
g.GenLiteralValueExpression(&buf, x)
return buf.String(), true
case *model.TemplateExpression:
if len(x.Parts) == 1 {
if lit, ok := x.Parts[0].(*model.LiteralValueExpression); ok && model.StringType.AssignableFrom(lit.Type()) {
strKey = lit.Value.AsString()
break
}
}
return "", false
default:
return "", false
}
return strKey, true
}
// functionName computes the go package, module, and name for the given function token.
func (g *generator) 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 := pcl.DecomposeToken(token, tokenRange)
if strings.HasPrefix(member, "get") {
if g.useLookupInvokeForm(token) {
member = strings.Replace(member, "get", "lookup", 1)
}
}
modOrAlias := g.getModOrAlias(pkg, module, module)
mod := strings.ReplaceAll(modOrAlias, "/", ".")
2020-10-06 05:57:00 +00:00
return pkg, mod, Title(member), diagnostics
}
var functionPackages = map[string][]string{
"join": {"strings"},
"mimeType": {"mime", "path"},
"readDir": {"os"},
"readFile": {"os"},
"filebase64": {"encoding/base64", "os"},
"toBase64": {"encoding/base64"},
2022-09-16 23:12:29 +00:00
"fromBase64": {"encoding/base64"},
"toJSON": {"encoding/json"},
"sha1": {"fmt", "crypto/sha1"},
"filebase64sha256": {"fmt", "crypto/sha256", "os"},
"cwd": {"os"},
"singleOrNone": {"fmt"},
}
func (g *generator) genFunctionPackages(x *model.FunctionCallExpression) []string {
return functionPackages[x.Name]
}