Merge branch 'master' of https://github.com/pulumi/pulumi into evan/gomod
This commit is contained in:
commit
ec686bbaf6
19
CHANGELOG.md
19
CHANGELOG.md
|
@ -1,7 +1,13 @@
|
|||
CHANGELOG
|
||||
=========
|
||||
|
||||
## HEAD (Unreleased)
|
||||
## 1.13.0 (2020-03-18)
|
||||
- Add support for plugin acquisition for Go programs
|
||||
[#4060](https://github.com/pulumi/pulumi/pull/4060)
|
||||
|
||||
- Display resource type in PAC violation output
|
||||
[#4061](https://github.com/pulumi/pulumi/issues/4061)
|
||||
|
||||
- Update to Helm v3 in pulumi Docker image
|
||||
[#4090](https://github.com/pulumi/pulumi/pull/4090)
|
||||
|
||||
|
@ -12,8 +18,14 @@ CHANGELOG
|
|||
[#4059](https://github.com/pulumi/pulumi/pull/4059)
|
||||
|
||||
- Add support for stack transformations in the .NET SDK.
|
||||
[4008](https://github.com/pulumi/pulumi/pull/4008)
|
||||
|
||||
[#4008](https://github.com/pulumi/pulumi/pull/4008)
|
||||
|
||||
- Fix `pulumi stack ls` on Windows
|
||||
[#4094](https://github.com/pulumi/pulumi/pull/4094)
|
||||
|
||||
- Add support for running Python policy packs.
|
||||
[#4057](https://github.com/pulumi/pulumi/pull/4057)
|
||||
|
||||
## 1.12.1 (2020-03-11)
|
||||
- Fix Kubernetes YAML parsing error in .NET.
|
||||
[#4023](https://github.com/pulumi/pulumi/pull/4023)
|
||||
|
@ -1630,4 +1642,3 @@ Resource naming is now more consistent, but there is a new file format for check
|
|||
- Support --since and --resource on `pulumi logs` when targeting the service
|
||||
- [Pulumi unable to serialize non-integers #694](https://github.com/pulumi/pulumi/issues/694)
|
||||
- [Stop buffering CLI output #660](https://github.com/pulumi/pulumi/issues/660)
|
||||
|
||||
|
|
4
Makefile
4
Makefile
|
@ -51,7 +51,9 @@ dist::
|
|||
go install -ldflags "-X github.com/pulumi/pulumi/sdk/go/common/version.Version=${VERSION}" ${PROJECT}
|
||||
|
||||
lint::
|
||||
golangci-lint run --deadline 5m
|
||||
for DIR in "examples" "pkg" "sdk" "tests" ; do \
|
||||
pushd $$DIR && golangci-lint run -c ../.golangci.yml --deadline 5m && popd ; \
|
||||
done
|
||||
|
||||
test_fast::
|
||||
$(GO_TEST_FAST) ${PROJECT_PKGS}
|
||||
|
|
|
@ -278,6 +278,15 @@
|
|||
|
||||
<!-- Tests -->
|
||||
<Target Name="Tests">
|
||||
<Exec Command="go test -v -timeout 5m -parallel $(TestParallelism) .\pkg\backend\..."
|
||||
IgnoreExitCode="true"
|
||||
WorkingDirectory="$(RepoRootDirectory)">
|
||||
<Output TaskParameter="ExitCode" PropertyName="BackendTestsExitCode" />
|
||||
</Exec>
|
||||
|
||||
<Error Text="backend tests (.\pkg\backend) failed"
|
||||
Condition="$(BackendTestsExitCode) != 0"/>
|
||||
|
||||
<Exec Command="go test -v -timeout 40m -cover -parallel $(TestParallelism) .\examples"
|
||||
IgnoreExitCode="true"
|
||||
WorkingDirectory="$(RepoRootDirectory)">
|
||||
|
|
|
@ -812,12 +812,13 @@ func (display *ProgressDisplay) printPolicyViolations() bool {
|
|||
c = colors.SpecError
|
||||
}
|
||||
|
||||
policyNameLine := fmt.Sprintf(" %s[%s] %s v%s %s %s (%s)",
|
||||
policyNameLine := fmt.Sprintf(" %s[%s] %s v%s %s %s (%s: %s)",
|
||||
c, policyEvent.EnforcementLevel,
|
||||
policyEvent.PolicyPackName,
|
||||
policyEvent.PolicyPackVersion, colors.Reset,
|
||||
policyEvent.PolicyName,
|
||||
policyEvent.ResourceURN.Name())
|
||||
policyEvent.ResourceURN.Name(),
|
||||
policyEvent.ResourceURN.Type())
|
||||
display.writeSimpleMessage(policyNameLine)
|
||||
|
||||
// The message may span multiple lines, so we massage it so it will be indented properly.
|
||||
|
|
|
@ -40,7 +40,9 @@ func (b *wrappedBucket) Delete(ctx context.Context, key string) (err error) {
|
|||
}
|
||||
|
||||
func (b *wrappedBucket) List(opts *blob.ListOptions) *blob.ListIterator {
|
||||
return b.bucket.List(opts)
|
||||
optsCopy := *opts
|
||||
optsCopy.Prefix = filepath.ToSlash(opts.Prefix)
|
||||
return b.bucket.List(&optsCopy)
|
||||
}
|
||||
|
||||
func (b *wrappedBucket) SignedURL(ctx context.Context, key string, opts *blob.SignedURLOptions) (string, error) {
|
||||
|
@ -83,7 +85,7 @@ func listBucket(bucket Bucket, dir string) ([]*blob.ListObject, error) {
|
|||
return files, nil
|
||||
}
|
||||
|
||||
// objectName returns the filename of a ListObject (an object from a bucket)
|
||||
// objectName returns the filename of a ListObject (an object from a bucket).
|
||||
func objectName(obj *blob.ListObject) string {
|
||||
_, filename := path.Split(obj.Key)
|
||||
return filename
|
||||
|
|
94
pkg/backend/filestate/bucket_test.go
Normal file
94
pkg/backend/filestate/bucket_test.go
Normal file
|
@ -0,0 +1,94 @@
|
|||
package filestate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"gocloud.dev/blob"
|
||||
)
|
||||
|
||||
func mustNotHaveError(t *testing.T, context string, err error) {
|
||||
t.Helper()
|
||||
if err != nil {
|
||||
t.Fatalf("Error in testcase %q, aborting: %v", context, err)
|
||||
}
|
||||
}
|
||||
|
||||
// The wrappedBucket type exists so that when we use the blob.Bucket type we can present a consistent
|
||||
// view of file paths. Since it will assume that backslashes (file separators on Windows) are part of
|
||||
// file names, and this causes "problems".
|
||||
func TestWrappedBucket(t *testing.T) {
|
||||
// wrappedBucket will only massage file paths IFF it is needed, as filepath.ToSlash is a noop.
|
||||
if filepath.Separator == '/' {
|
||||
assert.Equal(t, `foo\bar\baz`, filepath.ToSlash(`foo\bar\baz`))
|
||||
t.Skip("Skipping wrappedBucket tests because file paths won't be modified.")
|
||||
}
|
||||
|
||||
// Initialize a filestate backend, using the default Pulumi directory.
|
||||
cloudURL := FilePathPrefix + "~"
|
||||
b, err := New(nil, cloudURL)
|
||||
if err != nil {
|
||||
t.Fatalf("Initializing new filestate backend: %v", err)
|
||||
}
|
||||
localBackend, ok := b.(*localBackend)
|
||||
if !ok {
|
||||
t.Fatalf("backend wasn't of type localBackend?")
|
||||
}
|
||||
|
||||
wrappedBucket, ok := localBackend.bucket.(*wrappedBucket)
|
||||
if !ok {
|
||||
t.Fatalf("localBackend.bucket wasn't of type wrappedBucket?")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
// Perform basic file operations using wrappedBucket and verify that it will
|
||||
// successfully handle both "/" and "\" as file separators. (And probably fail in
|
||||
// exciting ways if you try to give it a file on a system that supports "\" or "/" as
|
||||
// a valid character in a filename.)
|
||||
t.Run("SanityCheck", func(t *testing.T) {
|
||||
randomData := []byte("Just some random data")
|
||||
|
||||
err := wrappedBucket.WriteAll(ctx, ".pulumi/bucket-test/foo", randomData, &blob.WriterOptions{})
|
||||
mustNotHaveError(t, "WriteAll", err)
|
||||
|
||||
readData, err := wrappedBucket.ReadAll(ctx, `.pulumi\bucket-test\foo`)
|
||||
mustNotHaveError(t, "ReadAll", err)
|
||||
assert.EqualValues(t, randomData, readData, "data read from bucket doesn't match what was written")
|
||||
|
||||
// Verify the leading slash isn't necessary.
|
||||
err = wrappedBucket.Delete(ctx, ".pulumi/bucket-test/foo")
|
||||
mustNotHaveError(t, "Delete", err)
|
||||
|
||||
exists, err := wrappedBucket.Exists(ctx, ".pulumi/bucket-test/foo")
|
||||
mustNotHaveError(t, "Exists", err)
|
||||
assert.False(t, exists, "Deleted file still found?")
|
||||
})
|
||||
|
||||
// Verify ListObjects / listBucket works with regard to differeing file separators too.
|
||||
t.Run("ListObjects", func(t *testing.T) {
|
||||
randomData := []byte("Just some random data")
|
||||
filenames := []string{"a.json", "b.json", "c.json"}
|
||||
|
||||
// Write some data.
|
||||
for _, filename := range filenames {
|
||||
key := fmt.Sprintf(`.pulumi\bucket-test\%s`, filename)
|
||||
err := wrappedBucket.WriteAll(ctx, key, randomData, &blob.WriterOptions{})
|
||||
mustNotHaveError(t, "WriteAll", err)
|
||||
}
|
||||
|
||||
// Verify it is found. NOTE: This requires that any files created
|
||||
// during other tests have successfully been cleaned up too.
|
||||
objects, err := listBucket(wrappedBucket, `.pulumi\bucket-test`)
|
||||
mustNotHaveError(t, "listBucket", err)
|
||||
if len(objects) != len(filenames) {
|
||||
assert.Equal(t, 3, len(objects), "listBucket returned unexpected number of objects.")
|
||||
for _, object := range objects {
|
||||
t.Logf("Got object: %+v", object)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
|
@ -730,7 +730,7 @@ func (pkg *pkgContext) genType(w io.Writer, obj *schema.ObjectType) {
|
|||
pkg.genOutputTypes(w, obj, pkg.details(obj))
|
||||
}
|
||||
|
||||
func (pkg *pkgContext) genInitFn(w io.Writer, types []*schema.ObjectType) {
|
||||
func (pkg *pkgContext) genTypeRegistrations(w io.Writer, types []*schema.ObjectType) {
|
||||
fmt.Fprintf(w, "func init() {\n")
|
||||
for _, obj := range types {
|
||||
name, details := pkg.tokenToType(obj.Token), pkg.details(obj)
|
||||
|
@ -912,6 +912,12 @@ func (pkg *pkgContext) genConfig(w io.Writer, variables []*schema.Property) erro
|
|||
return nil
|
||||
}
|
||||
|
||||
func (pkg *pkgContext) genPackageRegistration(w io.Writer) {
|
||||
fmt.Fprintf(w, "func init() {\n")
|
||||
fmt.Fprintf(w, "\tpulumi.RegisterPackage(pulumi.PackageInfo{Name:\"%s\", Version:\"%s\"})\n", pkg.pkg.Name, pkg.pkg.Version.String())
|
||||
fmt.Fprintf(w, "}\n")
|
||||
}
|
||||
|
||||
// GoInfo holds information required to generate the Go SDK from a schema.
|
||||
type GoInfo struct {
|
||||
// Base path for package imports
|
||||
|
@ -1058,7 +1064,7 @@ func GeneratePackage(tool string, pkg *schema.Package) (map[string][]byte, error
|
|||
files[relPath] = []byte(contents)
|
||||
}
|
||||
|
||||
name := pkg.Name
|
||||
name, registerPackage := pkg.Name, pkg.Provider != nil
|
||||
for _, mod := range pkgMods {
|
||||
pkg := packages[mod]
|
||||
|
||||
|
@ -1130,11 +1136,21 @@ func GeneratePackage(tool string, pkg *schema.Package) (map[string][]byte, error
|
|||
pkg.genType(buffer, t)
|
||||
}
|
||||
|
||||
pkg.genInitFn(buffer, pkg.types)
|
||||
pkg.genTypeRegistrations(buffer, pkg.types)
|
||||
|
||||
setFile(path.Join(mod, "pulumiTypes.go"), buffer.String())
|
||||
}
|
||||
|
||||
// Package registration
|
||||
if registerPackage {
|
||||
buffer := &bytes.Buffer{}
|
||||
|
||||
pkg.genHeader(buffer, []string{"github.com/pulumi/pulumi/sdk/go/pulumi"}, nil)
|
||||
pkg.genPackageRegistration(buffer)
|
||||
|
||||
setFile(path.Join(mod, "pulumiManifest.go"), buffer.String())
|
||||
}
|
||||
|
||||
// Utilities
|
||||
if pkg.needsUtils {
|
||||
buffer := &bytes.Buffer{}
|
||||
|
|
53
pkg/codegen/hcl2/model/attribute.go
Normal file
53
pkg/codegen/hcl2/model/attribute.go
Normal file
|
@ -0,0 +1,53 @@
|
|||
// 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 model
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||
"github.com/pulumi/pulumi/pkg/codegen/hcl2/syntax"
|
||||
)
|
||||
|
||||
// Attribute represents an HCL2 attribute.
|
||||
type Attribute struct {
|
||||
// The syntax node for the attribute, if any.
|
||||
Syntax *hclsyntax.Attribute
|
||||
// The tokens for the attribute.
|
||||
Tokens syntax.AttributeTokens
|
||||
|
||||
// The attribute's name.
|
||||
Name string
|
||||
// The attribute's value.
|
||||
Value Expression
|
||||
}
|
||||
|
||||
// SyntaxNode returns the syntax node of the attribute, and will either return an *hclsyntax.Attribute or syntax.None.
|
||||
func (a *Attribute) SyntaxNode() hclsyntax.Node {
|
||||
return syntaxOrNone(a.Syntax)
|
||||
}
|
||||
|
||||
func (*Attribute) isBodyItem() {}
|
||||
|
||||
// BindAttribute binds an HCL2 attribute using the given scope and token map.
|
||||
func BindAttribute(attribute *hclsyntax.Attribute, scope *Scope, tokens syntax.TokenMap) (*Attribute, hcl.Diagnostics) {
|
||||
value, diagnostics := BindExpression(attribute.Expr, scope, tokens)
|
||||
attributeTokens, _ := tokens.ForNode(attribute).(syntax.AttributeTokens)
|
||||
return &Attribute{
|
||||
Syntax: attribute,
|
||||
Tokens: attributeTokens,
|
||||
Name: attribute.Name,
|
||||
Value: value,
|
||||
}, diagnostics
|
||||
}
|
782
pkg/codegen/hcl2/model/binder_expression.go
Normal file
782
pkg/codegen/hcl2/model/binder_expression.go
Normal file
|
@ -0,0 +1,782 @@
|
|||
// 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 model
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||
"github.com/pulumi/pulumi/pkg/codegen/hcl2/syntax"
|
||||
"github.com/pulumi/pulumi/sdk/go/common/util/contract"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
"github.com/zclconf/go-cty/cty/convert"
|
||||
)
|
||||
|
||||
type expressionBinder struct {
|
||||
anonSymbols map[*hclsyntax.AnonSymbolExpr]Definition
|
||||
scope *Scope
|
||||
}
|
||||
|
||||
// BindExpression binds an HCL2 expression using the given scope and token map.
|
||||
func BindExpression(syntax hclsyntax.Node, scope *Scope, tokens syntax.TokenMap) (Expression, hcl.Diagnostics) {
|
||||
b := &expressionBinder{
|
||||
anonSymbols: map[*hclsyntax.AnonSymbolExpr]Definition{},
|
||||
scope: scope,
|
||||
}
|
||||
|
||||
return b.bindExpression(syntax)
|
||||
}
|
||||
|
||||
// BindExpressionText parses and binds an HCL2 expression using the given scope.
|
||||
func BindExpressionText(source string, scope *Scope) (Expression, hcl.Diagnostics) {
|
||||
syntax, tokens, diagnostics := syntax.ParseExpression(source, "<anonymous>", hcl.InitialPos)
|
||||
if diagnostics.HasErrors() {
|
||||
return nil, diagnostics
|
||||
}
|
||||
return BindExpression(syntax, scope, tokens)
|
||||
}
|
||||
|
||||
// bindExpression binds a single HCL2 expression.
|
||||
func (b *expressionBinder) bindExpression(syntax hclsyntax.Node) (Expression, hcl.Diagnostics) {
|
||||
switch syntax := syntax.(type) {
|
||||
case *hclsyntax.AnonSymbolExpr:
|
||||
return b.bindAnonSymbolExpression(syntax)
|
||||
case *hclsyntax.BinaryOpExpr:
|
||||
return b.bindBinaryOpExpression(syntax)
|
||||
case *hclsyntax.ConditionalExpr:
|
||||
return b.bindConditionalExpression(syntax)
|
||||
case *hclsyntax.ForExpr:
|
||||
return b.bindForExpression(syntax)
|
||||
case *hclsyntax.FunctionCallExpr:
|
||||
return b.bindFunctionCallExpression(syntax)
|
||||
case *hclsyntax.IndexExpr:
|
||||
return b.bindIndexExpression(syntax)
|
||||
case *hclsyntax.LiteralValueExpr:
|
||||
return b.bindLiteralValueExpression(syntax)
|
||||
case *hclsyntax.ObjectConsExpr:
|
||||
return b.bindObjectConsExpression(syntax)
|
||||
case *hclsyntax.ObjectConsKeyExpr:
|
||||
return b.bindObjectConsKeyExpr(syntax)
|
||||
case *hclsyntax.RelativeTraversalExpr:
|
||||
return b.bindRelativeTraversalExpression(syntax)
|
||||
case *hclsyntax.ScopeTraversalExpr:
|
||||
return b.bindScopeTraversalExpression(syntax)
|
||||
case *hclsyntax.SplatExpr:
|
||||
return b.bindSplatExpression(syntax)
|
||||
case *hclsyntax.TemplateExpr:
|
||||
return b.bindTemplateExpression(syntax)
|
||||
case *hclsyntax.TemplateJoinExpr:
|
||||
return b.bindTemplateJoinExpression(syntax)
|
||||
case *hclsyntax.TemplateWrapExpr:
|
||||
return b.bindTemplateWrapExpression(syntax)
|
||||
case *hclsyntax.TupleConsExpr:
|
||||
return b.bindTupleConsExpression(syntax)
|
||||
case *hclsyntax.UnaryOpExpr:
|
||||
return b.bindUnaryOpExpression(syntax)
|
||||
default:
|
||||
contract.Failf("unexpected expression node of type %T (%v)", syntax, syntax.Range())
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
// ctyTypeToType converts a cty.Type to a model Type.
|
||||
func ctyTypeToType(t cty.Type, optional bool) Type {
|
||||
// TODO(pdg): non-primitive types. We simply don't need these yet.
|
||||
var result Type
|
||||
switch {
|
||||
case t.Equals(cty.NilType):
|
||||
return NoneType
|
||||
case t.Equals(cty.Bool):
|
||||
result = BoolType
|
||||
case t.Equals(cty.Number):
|
||||
result = NumberType
|
||||
case t.Equals(cty.String):
|
||||
result = StringType
|
||||
case t.Equals(cty.DynamicPseudoType):
|
||||
result = DynamicType
|
||||
case t.IsMapType():
|
||||
result = NewMapType(ctyTypeToType(t.ElementType(), false))
|
||||
case t.IsListType():
|
||||
result = NewListType(ctyTypeToType(t.ElementType(), false))
|
||||
case t.IsSetType():
|
||||
result = NewSetType(ctyTypeToType(t.ElementType(), false))
|
||||
case t.IsObjectType():
|
||||
properties := map[string]Type{}
|
||||
for key, t := range t.AttributeTypes() {
|
||||
properties[key] = ctyTypeToType(t, false)
|
||||
}
|
||||
result = NewObjectType(properties)
|
||||
case t.IsTupleType():
|
||||
elements := make([]Type, len(t.TupleElementTypes()))
|
||||
for i, t := range t.TupleElementTypes() {
|
||||
elements[i] = ctyTypeToType(t, false)
|
||||
}
|
||||
result = NewTupleType(elements...)
|
||||
default:
|
||||
contract.Failf("NYI: cty type %v", t.FriendlyName())
|
||||
return NoneType
|
||||
}
|
||||
if optional {
|
||||
return NewOptionalType(result)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
var typeCapsule = cty.Capsule("type", reflect.TypeOf((*Type)(nil)).Elem())
|
||||
|
||||
// encapsulateType wraps the given type in a cty capsule for use in TraverseIndex values.
|
||||
func encapsulateType(t Type) cty.Value {
|
||||
return cty.CapsuleVal(typeCapsule, &t)
|
||||
}
|
||||
|
||||
// getOperationSignature returns the equivalent StaticFunctionSignature for a given Operation. This signature can be
|
||||
// used for typechecking the operation's arguments.
|
||||
func getOperationSignature(op *hclsyntax.Operation) StaticFunctionSignature {
|
||||
ctyParams := op.Impl.Params()
|
||||
|
||||
sig := StaticFunctionSignature{
|
||||
Parameters: make([]Parameter, len(ctyParams)),
|
||||
}
|
||||
for i, p := range ctyParams {
|
||||
sig.Parameters[i] = Parameter{
|
||||
Name: p.Name,
|
||||
Type: InputType(ctyTypeToType(p.Type, p.AllowNull)),
|
||||
}
|
||||
}
|
||||
if p := op.Impl.VarParam(); p != nil {
|
||||
sig.VarargsParameter = &Parameter{
|
||||
Name: p.Name,
|
||||
Type: InputType(ctyTypeToType(p.Type, p.AllowNull)),
|
||||
}
|
||||
}
|
||||
|
||||
sig.ReturnType = ctyTypeToType(op.Type, false)
|
||||
|
||||
return sig
|
||||
}
|
||||
|
||||
// typecheckArgs typechecks the arguments against a given function signature.
|
||||
func typecheckArgs(srcRange hcl.Range, signature StaticFunctionSignature, args ...Expression) hcl.Diagnostics {
|
||||
var diagnostics hcl.Diagnostics
|
||||
|
||||
// First typecheck the arguments for positional parameters. It is an error if there are fewer arguments than parameters
|
||||
// unless all missing arguments are for parameters with optional types.
|
||||
remainingArgs := args
|
||||
for _, param := range signature.Parameters {
|
||||
if len(remainingArgs) == 0 {
|
||||
if !IsOptionalType(param.Type) {
|
||||
diagnostics = append(diagnostics, missingRequiredArgument(param, srcRange))
|
||||
}
|
||||
} else {
|
||||
if !param.Type.ConversionFrom(remainingArgs[0].Type()).Exists() {
|
||||
diagnostics = append(diagnostics, ExprNotConvertible(param.Type, remainingArgs[0]))
|
||||
}
|
||||
remainingArgs = remainingArgs[1:]
|
||||
}
|
||||
}
|
||||
|
||||
// Typecheck any remaining arguments against the varargs parameter. It is an error if there is no varargs parameter.
|
||||
if len(remainingArgs) > 0 {
|
||||
varargs := signature.VarargsParameter
|
||||
if varargs == nil {
|
||||
diagnostics = append(diagnostics, extraArguments(len(signature.Parameters), len(args), srcRange))
|
||||
} else {
|
||||
for _, arg := range remainingArgs {
|
||||
if !varargs.Type.ConversionFrom(arg.Type()).Exists() {
|
||||
diagnostics = append(diagnostics, ExprNotConvertible(varargs.Type, arg))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return diagnostics
|
||||
}
|
||||
|
||||
// bindAnonSymbolExpression binds an anonymous symbol expression. These expressions should only occur in the context of
|
||||
// splat expressions, and are used represent the receiver of the expression following the splat. It is an error for
|
||||
// an anonymous symbol expression to occur outside this context.
|
||||
func (b *expressionBinder) bindAnonSymbolExpression(syntax *hclsyntax.AnonSymbolExpr) (Expression, hcl.Diagnostics) {
|
||||
var diagnostics hcl.Diagnostics
|
||||
|
||||
lv, ok := b.anonSymbols[syntax]
|
||||
if !ok {
|
||||
diagnostics = append(diagnostics, internalError(syntax.Range(), "undefined anonymous symbol"))
|
||||
return &ErrorExpression{Syntax: syntax, exprType: DynamicType}, diagnostics
|
||||
}
|
||||
|
||||
return &ScopeTraversalExpression{
|
||||
Syntax: &hclsyntax.ScopeTraversalExpr{
|
||||
Traversal: hcl.Traversal{hcl.TraverseRoot{Name: "<anonymous>", SrcRange: syntax.SrcRange}},
|
||||
SrcRange: syntax.SrcRange,
|
||||
},
|
||||
Parts: []Traversable{lv},
|
||||
}, diagnostics
|
||||
}
|
||||
|
||||
// bindBinaryOpExpression binds a binary operator expression. If the operands to the binary operator contain eventuals,
|
||||
// the result of the binary operator is eventual.
|
||||
func (b *expressionBinder) bindBinaryOpExpression(syntax *hclsyntax.BinaryOpExpr) (Expression, hcl.Diagnostics) {
|
||||
var diagnostics hcl.Diagnostics
|
||||
|
||||
// Bind the operands.
|
||||
leftOperand, leftDiags := b.bindExpression(syntax.LHS)
|
||||
diagnostics = append(diagnostics, leftDiags...)
|
||||
|
||||
rightOperand, rightDiags := b.bindExpression(syntax.RHS)
|
||||
diagnostics = append(diagnostics, rightDiags...)
|
||||
|
||||
// Compute the signature for the operator and typecheck the arguments.
|
||||
signature := getOperationSignature(syntax.Op)
|
||||
contract.Assert(len(signature.Parameters) == 2)
|
||||
|
||||
typecheckDiags := typecheckArgs(syntax.Range(), signature, leftOperand, rightOperand)
|
||||
diagnostics = append(diagnostics, typecheckDiags...)
|
||||
|
||||
return &BinaryOpExpression{
|
||||
Syntax: syntax,
|
||||
LeftOperand: leftOperand,
|
||||
RightOperand: rightOperand,
|
||||
exprType: liftOperationType(signature.ReturnType, leftOperand, rightOperand),
|
||||
}, diagnostics
|
||||
}
|
||||
|
||||
// bindConditionalExpression binds a conditional expression. The condition expression must be of type bool. The type of
|
||||
// the expression is unify(true result, false result). If the type of the condition is eventual, the type of the
|
||||
// expression is eventual.
|
||||
func (b *expressionBinder) bindConditionalExpression(syntax *hclsyntax.ConditionalExpr) (Expression, hcl.Diagnostics) {
|
||||
var diagnostics hcl.Diagnostics
|
||||
|
||||
// Bind the operands.
|
||||
condition, conditionDiags := b.bindExpression(syntax.Condition)
|
||||
diagnostics = append(diagnostics, conditionDiags...)
|
||||
|
||||
trueResult, trueDiags := b.bindExpression(syntax.TrueResult)
|
||||
diagnostics = append(diagnostics, trueDiags...)
|
||||
|
||||
falseResult, falseDiags := b.bindExpression(syntax.FalseResult)
|
||||
diagnostics = append(diagnostics, falseDiags...)
|
||||
|
||||
// Compute the type of the result.
|
||||
resultType, _ := UnifyTypes(trueResult.Type(), falseResult.Type())
|
||||
|
||||
// Typecheck the condition expression.
|
||||
signature := StaticFunctionSignature{Parameters: []Parameter{{Name: "condition", Type: InputType(BoolType)}}}
|
||||
typecheckDiags := typecheckArgs(syntax.Range(), signature, condition)
|
||||
diagnostics = append(diagnostics, typecheckDiags...)
|
||||
|
||||
return &ConditionalExpression{
|
||||
Syntax: syntax,
|
||||
Condition: condition,
|
||||
TrueResult: trueResult,
|
||||
FalseResult: falseResult,
|
||||
exprType: liftOperationType(resultType, condition),
|
||||
}, diagnostics
|
||||
}
|
||||
|
||||
// unwrapIterableSourceType removes any eventual types that wrap a type intended for iteration.
|
||||
func unwrapIterableSourceType(t Type) Type {
|
||||
// TODO(pdg): unions
|
||||
for {
|
||||
switch tt := t.(type) {
|
||||
case *OutputType:
|
||||
t = tt.ElementType
|
||||
case *PromiseType:
|
||||
t = tt.ElementType
|
||||
default:
|
||||
return t
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// wrapIterableSourceType adds optional or eventual types to a type intended for iteration per the structure of the
|
||||
// source type.
|
||||
func wrapIterableResultType(sourceType, iterableType Type) Type {
|
||||
// TODO(pdg): unions
|
||||
for {
|
||||
switch t := sourceType.(type) {
|
||||
case *OutputType:
|
||||
sourceType, iterableType = t.ElementType, NewOutputType(iterableType)
|
||||
case *PromiseType:
|
||||
sourceType, iterableType = t.ElementType, NewPromiseType(iterableType)
|
||||
default:
|
||||
return iterableType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// bindForExpression binds a for expression. The value being iterated must be an list, map, or object. The type of
|
||||
// the result is an list unless a key expression is present, in which case it is a map. Key types must be strings.
|
||||
// The element type of the result is the type of the value expression. If the type of the value being iterated is
|
||||
// optional or eventual, the type of the result is optional or eventual. If the type of the key expression or
|
||||
// condition expression is eventual, the result is also eventual.
|
||||
func (b *expressionBinder) bindForExpression(syntax *hclsyntax.ForExpr) (Expression, hcl.Diagnostics) {
|
||||
var diagnostics hcl.Diagnostics
|
||||
|
||||
collection, collectionDiags := b.bindExpression(syntax.CollExpr)
|
||||
diagnostics = append(diagnostics, collectionDiags...)
|
||||
|
||||
// Poke through any eventual and optional types that may wrap the collection type.
|
||||
collectionType := unwrapIterableSourceType(collection.Type())
|
||||
|
||||
// TODO(pdg): handle union types.
|
||||
|
||||
var keyType, valueType Type
|
||||
switch collectionType := collectionType.(type) {
|
||||
case *ListType:
|
||||
keyType, valueType = NumberType, collectionType.ElementType
|
||||
case *MapType:
|
||||
keyType, valueType = StringType, collectionType.ElementType
|
||||
case *TupleType:
|
||||
keyType = NumberType
|
||||
valueType, _ = UnifyTypes(collectionType.ElementTypes...)
|
||||
case *ObjectType:
|
||||
keyType = StringType
|
||||
|
||||
types := make([]Type, 0, len(collectionType.Properties))
|
||||
for _, t := range collectionType.Properties {
|
||||
types = append(types, t)
|
||||
}
|
||||
valueType, _ = UnifyTypes(types...)
|
||||
default:
|
||||
// If the collection is a dynamic type, treat it as an iterable(dynamic, dynamic). Otherwise, issue an error.
|
||||
if collectionType != DynamicType {
|
||||
diagnostics = append(diagnostics, unsupportedCollectionType(collectionType, syntax.CollExpr.Range()))
|
||||
}
|
||||
keyType, valueType = DynamicType, DynamicType
|
||||
}
|
||||
|
||||
// Push a scope for the key and value variables and define these vars.
|
||||
b.scope = b.scope.Push(syntax)
|
||||
defer func() { b.scope = b.scope.Pop() }()
|
||||
if syntax.KeyVar != "" {
|
||||
ok := b.scope.Define(syntax.KeyVar, &Variable{Name: syntax.KeyVar, VariableType: keyType})
|
||||
contract.Assert(ok)
|
||||
}
|
||||
if ok := b.scope.Define(syntax.ValVar, &Variable{Name: syntax.ValVar, VariableType: valueType}); !ok {
|
||||
diagnostics = append(diagnostics, nameAlreadyDefined(syntax.ValVar, syntax.Range()))
|
||||
}
|
||||
|
||||
var key Expression
|
||||
if syntax.KeyExpr != nil {
|
||||
keyExpr, keyDiags := b.bindExpression(syntax.KeyExpr)
|
||||
key, diagnostics = keyExpr, append(diagnostics, keyDiags...)
|
||||
|
||||
// A key expression is only present when producing a map. Key types must therefore be strings.
|
||||
if !InputType(StringType).ConversionFrom(key.Type()).Exists() {
|
||||
diagnostics = append(diagnostics, ExprNotConvertible(InputType(StringType), key))
|
||||
}
|
||||
}
|
||||
|
||||
value, valueDiags := b.bindExpression(syntax.ValExpr)
|
||||
diagnostics = append(diagnostics, valueDiags...)
|
||||
|
||||
var condition Expression
|
||||
if syntax.CondExpr != nil {
|
||||
condExpr, conditionDiags := b.bindExpression(syntax.CondExpr)
|
||||
condition, diagnostics = condExpr, append(diagnostics, conditionDiags...)
|
||||
|
||||
if !InputType(BoolType).ConversionFrom(condition.Type()).Exists() {
|
||||
diagnostics = append(diagnostics, ExprNotConvertible(InputType(BoolType), condition))
|
||||
}
|
||||
}
|
||||
|
||||
// If there is a key expression, we are producing a map. Otherwise, we are producing an list. In either case, wrap
|
||||
// the result type in the same set of eventuals and optionals present in the collection type.
|
||||
var resultType Type
|
||||
if key != nil {
|
||||
valueType := value.Type()
|
||||
if syntax.Group {
|
||||
valueType = NewListType(valueType)
|
||||
}
|
||||
resultType = wrapIterableResultType(collection.Type(), NewMapType(valueType))
|
||||
} else {
|
||||
resultType = wrapIterableResultType(collection.Type(), NewListType(value.Type()))
|
||||
}
|
||||
|
||||
// If either the key expression or the condition expression is eventual, the result is eventual: each of these
|
||||
// values is required to determine which items are present in the result.
|
||||
var liftArgs []Expression
|
||||
if key != nil {
|
||||
liftArgs = append(liftArgs, key)
|
||||
}
|
||||
if condition != nil {
|
||||
liftArgs = append(liftArgs, condition)
|
||||
}
|
||||
|
||||
return &ForExpression{
|
||||
Syntax: syntax,
|
||||
Collection: collection,
|
||||
Key: key,
|
||||
Value: value,
|
||||
exprType: liftOperationType(resultType, liftArgs...),
|
||||
}, diagnostics
|
||||
}
|
||||
|
||||
// bindFunctionCallExpression binds a function call expression. The name of the function is bound using the current
|
||||
// scope chain. An argument to a function must be assignable to the type of the corresponding parameter. It is not an
|
||||
// error to omit arguments for trailing positional parameters if those parameters are optional. If any of the
|
||||
// parameters to the function are eventual, the result type of the function is also eventual.
|
||||
func (b *expressionBinder) bindFunctionCallExpression(
|
||||
syntax *hclsyntax.FunctionCallExpr) (Expression, hcl.Diagnostics) {
|
||||
|
||||
var diagnostics hcl.Diagnostics
|
||||
|
||||
// Bind the function's arguments.
|
||||
args := make([]Expression, len(syntax.Args))
|
||||
for i, syntax := range syntax.Args {
|
||||
arg, argDiagnostics := b.bindExpression(syntax)
|
||||
args[i], diagnostics = arg, append(diagnostics, argDiagnostics...)
|
||||
}
|
||||
|
||||
// Attempt to bind the name of the function to its definition.
|
||||
function, hasFunction := b.scope.BindFunctionReference(syntax.Name)
|
||||
if !hasFunction {
|
||||
diagnostics = append(diagnostics, unknownFunction(syntax.Name, syntax.NameRange))
|
||||
|
||||
return &FunctionCallExpression{
|
||||
Syntax: syntax,
|
||||
Name: syntax.Name,
|
||||
Signature: StaticFunctionSignature{
|
||||
VarargsParameter: &Parameter{Name: "args", Type: DynamicType},
|
||||
ReturnType: DynamicType,
|
||||
},
|
||||
Args: args,
|
||||
}, diagnostics
|
||||
}
|
||||
|
||||
// Compute the function's signature.
|
||||
signature, sigDiags := function.GetSignature(args)
|
||||
diagnostics = append(diagnostics, sigDiags...)
|
||||
|
||||
// Wrap the function's parameter types in input types s.t. they accept eventual arguments.
|
||||
for i := range signature.Parameters {
|
||||
signature.Parameters[i].Type = InputType(signature.Parameters[i].Type)
|
||||
}
|
||||
if signature.VarargsParameter != nil {
|
||||
signature.VarargsParameter.Type = InputType(signature.VarargsParameter.Type)
|
||||
}
|
||||
|
||||
// Typecheck the function's arguments.
|
||||
typecheckDiags := typecheckArgs(syntax.Range(), signature, args...)
|
||||
diagnostics = append(diagnostics, typecheckDiags...)
|
||||
|
||||
signature.ReturnType = liftOperationType(signature.ReturnType, args...)
|
||||
|
||||
return &FunctionCallExpression{
|
||||
Syntax: syntax,
|
||||
Name: syntax.Name,
|
||||
Signature: signature,
|
||||
Args: args,
|
||||
}, diagnostics
|
||||
}
|
||||
|
||||
// bindIndexExpression binds an index expression. The value being indexed must be an list, map, or object.
|
||||
//
|
||||
// - If the value is an list, the result type is the type of the list's elements, and the index must be assignable to
|
||||
// number (TODO(pdg): require integer indices?)
|
||||
// - If the value is a map, the result type is the type of the map's values, and the index must be assignable to
|
||||
// string
|
||||
// - If the value is an object, the result type is any, and the index must be assignable to a string
|
||||
//
|
||||
// If either the value being indexed or the index is eventual, result is eventual.
|
||||
func (b *expressionBinder) bindIndexExpression(syntax *hclsyntax.IndexExpr) (Expression, hcl.Diagnostics) {
|
||||
var diagnostics hcl.Diagnostics
|
||||
|
||||
collection, collectionDiags := b.bindExpression(syntax.Collection)
|
||||
diagnostics = append(diagnostics, collectionDiags...)
|
||||
|
||||
key, keyDiags := b.bindExpression(syntax.Key)
|
||||
diagnostics = append(diagnostics, keyDiags...)
|
||||
|
||||
part, partDiags := collection.Type().Traverse(hcl.TraverseIndex{
|
||||
Key: encapsulateType(key.Type()),
|
||||
SrcRange: syntax.Key.Range(),
|
||||
})
|
||||
|
||||
diagnostics = append(diagnostics, partDiags...)
|
||||
|
||||
return &IndexExpression{
|
||||
Syntax: syntax,
|
||||
Collection: collection,
|
||||
Key: key,
|
||||
exprType: liftOperationType(part.(Type), collection, key),
|
||||
}, diagnostics
|
||||
}
|
||||
|
||||
// bindLiteralValueExpression binds a literal value expression. The value must be a boolean, integer, number, or
|
||||
// string.
|
||||
func (b *expressionBinder) bindLiteralValueExpression(
|
||||
syntax *hclsyntax.LiteralValueExpr) (Expression, hcl.Diagnostics) {
|
||||
|
||||
v, typ, diagnostics := syntax.Val, NoneType, hcl.Diagnostics(nil)
|
||||
if !v.IsNull() {
|
||||
typ = ctyTypeToType(v.Type(), false)
|
||||
}
|
||||
|
||||
switch {
|
||||
case typ == NoneType || typ == StringType || typ == IntType || typ == NumberType || typ == BoolType:
|
||||
// OK
|
||||
default:
|
||||
typ, diagnostics = DynamicType, hcl.Diagnostics{unsupportedLiteralValue(syntax)}
|
||||
}
|
||||
|
||||
return &LiteralValueExpression{
|
||||
Syntax: syntax,
|
||||
Value: v,
|
||||
exprType: typ,
|
||||
}, diagnostics
|
||||
}
|
||||
|
||||
// bindObjectConsExpression binds an object construction expression. The expression's keys must be strings. If any of
|
||||
// the keys are not literal values, the result type is map(U), where U is the unified type of the property types.
|
||||
// Otherwise, the result type is an object type that maps each key to the type of its value. If any of the keys is
|
||||
// eventual, the result is eventual.
|
||||
func (b *expressionBinder) bindObjectConsExpression(syntax *hclsyntax.ObjectConsExpr) (Expression, hcl.Diagnostics) {
|
||||
var diagnostics hcl.Diagnostics
|
||||
|
||||
keys := make([]Expression, len(syntax.Items))
|
||||
items := make([]ObjectConsItem, len(syntax.Items))
|
||||
for i, item := range syntax.Items {
|
||||
keyExpr, keyDiags := b.bindExpression(item.KeyExpr)
|
||||
diagnostics = append(diagnostics, keyDiags...)
|
||||
keys[i] = keyExpr
|
||||
|
||||
if !InputType(StringType).ConversionFrom(keyExpr.Type()).Exists() {
|
||||
diagnostics = append(diagnostics, objectKeysMustBeStrings(keyExpr))
|
||||
}
|
||||
|
||||
valExpr, valDiags := b.bindExpression(item.ValueExpr)
|
||||
diagnostics = append(diagnostics, valDiags...)
|
||||
|
||||
items[i] = ObjectConsItem{Key: keyExpr, Value: valExpr}
|
||||
}
|
||||
|
||||
// Attempt to build an object type out of the result. If there are any attribute names that come from variables,
|
||||
// type the result as map(unify(propertyTypes)).
|
||||
properties, isMapType, types := map[string]Type{}, false, []Type{}
|
||||
for _, item := range items {
|
||||
types = append(types, item.Value.Type())
|
||||
|
||||
keyLit, ok := item.Key.(*LiteralValueExpression)
|
||||
if ok {
|
||||
key, err := convert.Convert(keyLit.Value, cty.String)
|
||||
if err == nil {
|
||||
properties[key.AsString()] = item.Value.Type()
|
||||
continue
|
||||
}
|
||||
}
|
||||
isMapType = true
|
||||
}
|
||||
var typ Type
|
||||
if isMapType {
|
||||
elementType, _ := UnifyTypes(types...)
|
||||
typ = NewMapType(elementType)
|
||||
} else {
|
||||
typ = NewObjectType(properties)
|
||||
}
|
||||
|
||||
return &ObjectConsExpression{
|
||||
Syntax: syntax,
|
||||
Items: items,
|
||||
exprType: liftOperationType(typ, keys...),
|
||||
}, diagnostics
|
||||
}
|
||||
|
||||
// bindObjectConsKeyExpr binds an object construction key.
|
||||
func (b *expressionBinder) bindObjectConsKeyExpr(syntax *hclsyntax.ObjectConsKeyExpr) (Expression, hcl.Diagnostics) {
|
||||
if !syntax.ForceNonLiteral {
|
||||
if name := hcl.ExprAsKeyword(syntax); name != "" {
|
||||
return b.bindExpression(&hclsyntax.LiteralValueExpr{
|
||||
Val: cty.StringVal(name),
|
||||
SrcRange: syntax.Range(),
|
||||
})
|
||||
}
|
||||
}
|
||||
return b.bindExpression(syntax.Wrapped)
|
||||
}
|
||||
|
||||
// bindRelativeTraversalExpression binds a relative traversal expression. Each step of the traversal must be a legal
|
||||
// step with respect to its receiver. The root receiver is the type of the source expression.
|
||||
func (b *expressionBinder) bindRelativeTraversalExpression(
|
||||
syntax *hclsyntax.RelativeTraversalExpr) (Expression, hcl.Diagnostics) {
|
||||
|
||||
source, diagnostics := b.bindExpression(syntax.Source)
|
||||
|
||||
parts, partDiags := b.bindTraversalParts(source.Type(), syntax.Traversal)
|
||||
diagnostics = append(diagnostics, partDiags...)
|
||||
|
||||
return &RelativeTraversalExpression{
|
||||
Syntax: syntax,
|
||||
Source: source,
|
||||
Parts: parts,
|
||||
}, diagnostics
|
||||
}
|
||||
|
||||
// bindScopeTraversalExpression binds a scope traversal expression. Each step of the traversal must be a legal
|
||||
// step with respect to its receiver. The root receiver is the definition in the current scope referred to by the root
|
||||
// name.
|
||||
func (b *expressionBinder) bindScopeTraversalExpression(
|
||||
syntax *hclsyntax.ScopeTraversalExpr) (Expression, hcl.Diagnostics) {
|
||||
|
||||
def, ok := b.scope.BindReference(syntax.Traversal.RootName())
|
||||
if !ok {
|
||||
parts := make([]Traversable, len(syntax.Traversal))
|
||||
for i := range parts {
|
||||
parts[i] = DynamicType
|
||||
}
|
||||
return &ScopeTraversalExpression{
|
||||
Syntax: syntax,
|
||||
Parts: parts,
|
||||
}, hcl.Diagnostics{undefinedVariable(syntax.Traversal.SimpleSplit().Abs.SourceRange())}
|
||||
}
|
||||
|
||||
parts, diagnostics := b.bindTraversalParts(def, syntax.Traversal.SimpleSplit().Rel)
|
||||
return &ScopeTraversalExpression{
|
||||
Syntax: syntax,
|
||||
Parts: parts,
|
||||
}, diagnostics
|
||||
}
|
||||
|
||||
// bindSplatExpression binds a splat expression. If the type being splatted is an list or any, the type of the
|
||||
// iteration variable is the element type of the list or any, respectively. Otherwise, the type of the iteration
|
||||
// variable is the type being splatted: in this case, the splat expression implicitly constructs a single-element
|
||||
// tuple. The type of the result is an list whose elements have the type of the each expression. If the value being
|
||||
// splatted is eventual or optional, the result type is eventual or optional.
|
||||
func (b *expressionBinder) bindSplatExpression(syntax *hclsyntax.SplatExpr) (Expression, hcl.Diagnostics) {
|
||||
var diagnostics hcl.Diagnostics
|
||||
|
||||
source, sourceDiags := b.bindExpression(syntax.Source)
|
||||
diagnostics = append(diagnostics, sourceDiags...)
|
||||
|
||||
sourceType := unwrapIterableSourceType(source.Type())
|
||||
elementType := sourceType
|
||||
if arr, isList := sourceType.(*ListType); isList {
|
||||
elementType = arr.ElementType
|
||||
} else if sourceType != DynamicType {
|
||||
source = &TupleConsExpression{
|
||||
Syntax: &hclsyntax.TupleConsExpr{
|
||||
Exprs: []hclsyntax.Expression{syntax.Source},
|
||||
SrcRange: syntax.Source.Range(),
|
||||
OpenRange: syntax.Source.StartRange(),
|
||||
},
|
||||
Expressions: []Expression{source},
|
||||
exprType: NewListType(source.Type()),
|
||||
}
|
||||
}
|
||||
|
||||
item := &Variable{
|
||||
Name: "<anonymous>",
|
||||
VariableType: elementType,
|
||||
}
|
||||
b.anonSymbols[syntax.Item] = item
|
||||
|
||||
each, eachDiags := b.bindExpression(syntax.Each)
|
||||
diagnostics = append(diagnostics, eachDiags...)
|
||||
|
||||
return &SplatExpression{
|
||||
Syntax: syntax,
|
||||
Source: source,
|
||||
Each: each,
|
||||
Item: item,
|
||||
exprType: wrapIterableResultType(source.Type(), NewListType(each.Type())),
|
||||
}, diagnostics
|
||||
}
|
||||
|
||||
// bindTemplateExpression binds a template expression. The result is always a string. If any of the parts of the
|
||||
// expression are eventual, the result is eventual.
|
||||
func (b *expressionBinder) bindTemplateExpression(syntax *hclsyntax.TemplateExpr) (Expression, hcl.Diagnostics) {
|
||||
if syntax.IsStringLiteral() {
|
||||
return b.bindExpression(syntax.Parts[0])
|
||||
}
|
||||
|
||||
var diagnostics hcl.Diagnostics
|
||||
parts := make([]Expression, len(syntax.Parts))
|
||||
for i, syntax := range syntax.Parts {
|
||||
part, partDiags := b.bindExpression(syntax)
|
||||
parts[i], diagnostics = part, append(diagnostics, partDiags...)
|
||||
}
|
||||
|
||||
return &TemplateExpression{
|
||||
Syntax: syntax,
|
||||
Parts: parts,
|
||||
exprType: liftOperationType(StringType, parts...),
|
||||
}, diagnostics
|
||||
}
|
||||
|
||||
// bindTemplateJoinExpression binds a template join expression. If any of the parts of the expression are eventual,
|
||||
// the result is eventual.
|
||||
func (b *expressionBinder) bindTemplateJoinExpression(
|
||||
syntax *hclsyntax.TemplateJoinExpr) (Expression, hcl.Diagnostics) {
|
||||
|
||||
tuple, diagnostics := b.bindExpression(syntax.Tuple)
|
||||
|
||||
return &TemplateJoinExpression{
|
||||
Syntax: syntax,
|
||||
Tuple: tuple,
|
||||
exprType: liftOperationType(StringType, tuple),
|
||||
}, diagnostics
|
||||
}
|
||||
|
||||
// bindTemplateWrapExpression binds a template wrap expression.
|
||||
func (b *expressionBinder) bindTemplateWrapExpression(
|
||||
syntax *hclsyntax.TemplateWrapExpr) (Expression, hcl.Diagnostics) {
|
||||
|
||||
return b.bindExpression(syntax.Wrapped)
|
||||
}
|
||||
|
||||
// bindTupleConsExpression binds a tuple construction expression. The result is a tuple(T_0, ..., T_N).
|
||||
func (b *expressionBinder) bindTupleConsExpression(syntax *hclsyntax.TupleConsExpr) (Expression, hcl.Diagnostics) {
|
||||
var diagnostics hcl.Diagnostics
|
||||
exprs := make([]Expression, len(syntax.Exprs))
|
||||
for i, syntax := range syntax.Exprs {
|
||||
expr, exprDiags := b.bindExpression(syntax)
|
||||
exprs[i], diagnostics = expr, append(diagnostics, exprDiags...)
|
||||
}
|
||||
|
||||
elementTypes := make([]Type, len(exprs))
|
||||
for i, expr := range exprs {
|
||||
elementTypes[i] = expr.Type()
|
||||
}
|
||||
|
||||
return &TupleConsExpression{
|
||||
Syntax: syntax,
|
||||
Expressions: exprs,
|
||||
exprType: NewTupleType(elementTypes...),
|
||||
}, diagnostics
|
||||
}
|
||||
|
||||
// bindUnaryOpExpression binds a unary operator expression. If the operand to the unary operator contains eventuals,
|
||||
// the result of the unary operator is eventual.
|
||||
func (b *expressionBinder) bindUnaryOpExpression(syntax *hclsyntax.UnaryOpExpr) (Expression, hcl.Diagnostics) {
|
||||
var diagnostics hcl.Diagnostics
|
||||
|
||||
// Bind the operand.
|
||||
operand, operandDiags := b.bindExpression(syntax.Val)
|
||||
diagnostics = append(diagnostics, operandDiags...)
|
||||
|
||||
// Compute the signature for the operator and typecheck the arguments.
|
||||
signature := getOperationSignature(syntax.Op)
|
||||
contract.Assert(len(signature.Parameters) == 1)
|
||||
|
||||
typecheckDiags := typecheckArgs(syntax.Range(), signature, operand)
|
||||
diagnostics = append(diagnostics, typecheckDiags...)
|
||||
|
||||
return &UnaryOpExpression{
|
||||
Syntax: syntax,
|
||||
Operand: operand,
|
||||
exprType: liftOperationType(signature.ReturnType, operand),
|
||||
}, diagnostics
|
||||
}
|
695
pkg/codegen/hcl2/model/binder_expression_test.go
Normal file
695
pkg/codegen/hcl2/model/binder_expression_test.go
Normal file
|
@ -0,0 +1,695 @@
|
|||
// 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 model
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/pulumi/pulumi/pkg/codegen/hcl2/syntax"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
func TestBindLiteral(t *testing.T) {
|
||||
expr, diags := BindExpressionText("false", nil)
|
||||
assert.Len(t, diags, 0)
|
||||
assert.Equal(t, BoolType, expr.Type())
|
||||
lit, ok := expr.(*LiteralValueExpression)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, cty.False, lit.Value)
|
||||
|
||||
expr, diags = BindExpressionText("true", nil)
|
||||
assert.Len(t, diags, 0)
|
||||
assert.Equal(t, BoolType, expr.Type())
|
||||
lit, ok = expr.(*LiteralValueExpression)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, cty.True, lit.Value)
|
||||
|
||||
expr, diags = BindExpressionText("0", nil)
|
||||
assert.Len(t, diags, 0)
|
||||
assert.Equal(t, NumberType, expr.Type())
|
||||
lit, ok = expr.(*LiteralValueExpression)
|
||||
assert.True(t, ok)
|
||||
assert.True(t, cty.NumberIntVal(0).RawEquals(lit.Value))
|
||||
|
||||
expr, diags = BindExpressionText("3.14", nil)
|
||||
assert.Len(t, diags, 0)
|
||||
assert.Equal(t, NumberType, expr.Type())
|
||||
lit, ok = expr.(*LiteralValueExpression)
|
||||
assert.True(t, ok)
|
||||
assert.True(t, cty.MustParseNumberVal("3.14").RawEquals(lit.Value))
|
||||
|
||||
expr, diags = BindExpressionText(`"foo"`, nil)
|
||||
assert.Len(t, diags, 0)
|
||||
assert.Equal(t, StringType, expr.Type())
|
||||
lit, ok = expr.(*LiteralValueExpression)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, cty.StringVal("foo"), lit.Value)
|
||||
}
|
||||
|
||||
type environment map[string]interface{}
|
||||
|
||||
func (e environment) scope() *Scope {
|
||||
s := NewRootScope(syntax.None)
|
||||
for name, typeOrFunction := range e {
|
||||
switch typeOrFunction := typeOrFunction.(type) {
|
||||
case *Function:
|
||||
s.DefineFunction(name, typeOrFunction)
|
||||
case Type:
|
||||
s.Define(name, &Variable{Name: name, VariableType: typeOrFunction})
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
type exprTestCase struct {
|
||||
x string
|
||||
t Type
|
||||
xt Expression
|
||||
}
|
||||
|
||||
func TestBindBinaryOp(t *testing.T) {
|
||||
env := environment(map[string]interface{}{
|
||||
"a": NewOutputType(BoolType),
|
||||
"b": NewPromiseType(BoolType),
|
||||
"c": NewOutputType(NumberType),
|
||||
"d": NewPromiseType(NumberType),
|
||||
})
|
||||
scope := env.scope()
|
||||
|
||||
cases := []exprTestCase{
|
||||
// Comparisons
|
||||
{x: "0 == 0", t: BoolType},
|
||||
{x: "0 != 0", t: BoolType},
|
||||
{x: "0 < 0", t: BoolType},
|
||||
{x: "0 > 0", t: BoolType},
|
||||
{x: "0 <= 0", t: BoolType},
|
||||
{x: "0 >= 0", t: BoolType},
|
||||
|
||||
// Arithmetic
|
||||
{x: "0 + 0", t: NumberType},
|
||||
{x: "0 - 0", t: NumberType},
|
||||
{x: "0 * 0", t: NumberType},
|
||||
{x: "0 / 0", t: NumberType},
|
||||
{x: "0 % 0", t: NumberType},
|
||||
|
||||
// Logical
|
||||
{x: "false && false", t: BoolType},
|
||||
{x: "false || false", t: BoolType},
|
||||
|
||||
// Lifted operations
|
||||
{x: "a == true", t: NewOutputType(BoolType)},
|
||||
{x: "b == true", t: NewPromiseType(BoolType)},
|
||||
{x: "c + 0", t: NewOutputType(NumberType)},
|
||||
{x: "d + 0", t: NewPromiseType(NumberType)},
|
||||
{x: "a && true", t: NewOutputType(BoolType)},
|
||||
{x: "b && true", t: NewPromiseType(BoolType)},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.x, func(t *testing.T) {
|
||||
expr, diags := BindExpressionText(c.x, scope)
|
||||
assert.Len(t, diags, 0)
|
||||
assert.Equal(t, c.t, expr.Type())
|
||||
_, ok := expr.(*BinaryOpExpression)
|
||||
assert.True(t, ok)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBindConditional(t *testing.T) {
|
||||
env := environment(map[string]interface{}{
|
||||
"a": NewOutputType(BoolType),
|
||||
"b": NewPromiseType(BoolType),
|
||||
})
|
||||
scope := env.scope()
|
||||
|
||||
cases := []exprTestCase{
|
||||
{x: "true ? 0 : 1", t: NumberType},
|
||||
{x: "true ? 0 : false", t: NewUnionType(NumberType, BoolType)},
|
||||
{x: "true ? a : b", t: NewOutputType(BoolType)},
|
||||
|
||||
// Lifted operations
|
||||
{x: "a ? 0 : 1", t: NewOutputType(NumberType)},
|
||||
{x: "b ? 0 : 1", t: NewPromiseType(NumberType)},
|
||||
{x: "a ? 0 : false", t: NewOutputType(NewUnionType(NumberType, BoolType))},
|
||||
{x: "b ? 0 : false", t: NewPromiseType(NewUnionType(NumberType, BoolType))},
|
||||
{x: "a ? a : b", t: NewOutputType(BoolType)},
|
||||
{x: "b ? b : b", t: NewPromiseType(BoolType)},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.x, func(t *testing.T) {
|
||||
expr, diags := BindExpressionText(c.x, scope)
|
||||
assert.Len(t, diags, 0)
|
||||
assert.Equal(t, c.t, expr.Type())
|
||||
_, ok := expr.(*ConditionalExpression)
|
||||
assert.True(t, ok)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBindFor(t *testing.T) {
|
||||
// TODO: union collection types
|
||||
|
||||
env := environment(map[string]interface{}{
|
||||
"a": NewMapType(StringType),
|
||||
"aa": NewMapType(NewOutputType(StringType)),
|
||||
"b": NewOutputType(NewMapType(StringType)),
|
||||
"c": NewPromiseType(NewMapType(StringType)),
|
||||
"d": NewListType(StringType),
|
||||
"dd": NewListType(NewOutputType(StringType)),
|
||||
"e": NewOutputType(NewListType(StringType)),
|
||||
"f": NewPromiseType(NewListType(StringType)),
|
||||
"g": BoolType,
|
||||
"h": NewOutputType(BoolType),
|
||||
"i": NewPromiseType(BoolType),
|
||||
"j": StringType,
|
||||
"k": NewOutputType(StringType),
|
||||
"l": NewPromiseType(StringType),
|
||||
})
|
||||
scope := env.scope()
|
||||
|
||||
cases := []exprTestCase{
|
||||
// Object for
|
||||
{x: `{for k, v in {}: k => v}`, t: NewMapType(NoneType)},
|
||||
{x: `{for k, v in {foo: "bar"}: k => v}`, t: NewMapType(StringType)},
|
||||
{x: `{for k, v in {foo: "bar"}: k => 0}`, t: NewMapType(NumberType)},
|
||||
{x: `{for k, v in {foo: 0}: k => v}`, t: NewMapType(NumberType)},
|
||||
{x: `{for k, v in a: k => v}`, t: NewMapType(StringType)},
|
||||
{x: `{for k, v in aa: k => v}`, t: NewMapType(NewOutputType(StringType))},
|
||||
{x: `{for k, v in a: k => 0}`, t: NewMapType(NumberType)},
|
||||
{x: `{for k, v in d: v => k}`, t: NewMapType(NumberType)},
|
||||
{x: `{for k, v in d: v => k...}`, t: NewMapType(NewListType(NumberType))},
|
||||
{x: `{for k, v in d: v => k if k > 10}`, t: NewMapType(NumberType)},
|
||||
|
||||
// List for
|
||||
{x: `[for k, v in {}: [k, v]]`, t: NewListType(NewTupleType(StringType, NoneType))},
|
||||
{x: `[for k, _ in {}: k]`, t: NewListType(StringType)},
|
||||
{x: `[for v in []: v]`, t: NewListType(NoneType)},
|
||||
|
||||
// Lifted operations
|
||||
{x: `{for k, v in b: k => v}`, t: NewOutputType(NewMapType(StringType))},
|
||||
{x: `{for k, v in c: k => v}`, t: NewPromiseType(NewMapType(StringType))},
|
||||
{x: `{for k, v in {}: k => v if h}`, t: NewOutputType(NewMapType(NoneType))},
|
||||
{x: `{for k, v in {}: k => v if i}`, t: NewPromiseType(NewMapType(NoneType))},
|
||||
{x: `[for v in e: v]`, t: NewOutputType(NewListType(StringType))},
|
||||
{x: `[for v in f: v]`, t: NewPromiseType(NewListType(StringType))},
|
||||
{x: `[for v in []: v if h]`, t: NewOutputType(NewListType(NoneType))},
|
||||
{x: `[for v in []: v if i]`, t: NewPromiseType(NewListType(NoneType))},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.x, func(t *testing.T) {
|
||||
expr, diags := BindExpressionText(c.x, scope)
|
||||
assert.Len(t, diags, 0)
|
||||
assert.Equal(t, c.t, expr.Type())
|
||||
_, ok := expr.(*ForExpression)
|
||||
assert.True(t, ok)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBindFunctionCall(t *testing.T) {
|
||||
env := environment(map[string]interface{}{
|
||||
"f0": NewFunction(StaticFunctionSignature{
|
||||
Parameters: []Parameter{
|
||||
{Name: "foo", Type: StringType},
|
||||
{Name: "bar", Type: IntType},
|
||||
},
|
||||
ReturnType: BoolType,
|
||||
}),
|
||||
"f1": NewFunction(StaticFunctionSignature{
|
||||
Parameters: []Parameter{
|
||||
{Name: "foo", Type: StringType},
|
||||
},
|
||||
VarargsParameter: &Parameter{
|
||||
Name: "bar", Type: IntType,
|
||||
},
|
||||
ReturnType: BoolType,
|
||||
}),
|
||||
"a": NewOutputType(StringType),
|
||||
"b": NewPromiseType(StringType),
|
||||
"c": NewOutputType(IntType),
|
||||
"d": NewPromiseType(IntType),
|
||||
})
|
||||
scope := env.scope()
|
||||
|
||||
cases := []exprTestCase{
|
||||
// Standard calls
|
||||
{x: `f0("foo", 0)`, t: BoolType},
|
||||
{x: `f1("foo")`, t: BoolType},
|
||||
{x: `f1("foo", 1, 2, 3)`, t: BoolType},
|
||||
|
||||
// Lifted calls
|
||||
{x: `f0(a, 0)`, t: NewOutputType(BoolType)},
|
||||
{x: `f0(b, 0)`, t: NewPromiseType(BoolType)},
|
||||
{x: `f0("foo", c)`, t: NewOutputType(BoolType)},
|
||||
{x: `f0("foo", d)`, t: NewPromiseType(BoolType)},
|
||||
{x: `f0(a, d)`, t: NewOutputType(BoolType)},
|
||||
{x: `f0(b, c)`, t: NewOutputType(BoolType)},
|
||||
{x: `f1(a)`, t: NewOutputType(BoolType)},
|
||||
{x: `f1(b)`, t: NewPromiseType(BoolType)},
|
||||
{x: `f1("foo", c)`, t: NewOutputType(BoolType)},
|
||||
{x: `f1("foo", d)`, t: NewPromiseType(BoolType)},
|
||||
{x: `f1("foo", c, d)`, t: NewOutputType(BoolType)},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.x, func(t *testing.T) {
|
||||
expr, diags := BindExpressionText(c.x, scope)
|
||||
assert.Len(t, diags, 0)
|
||||
assert.Equal(t, c.t, expr.Type())
|
||||
_, ok := expr.(*FunctionCallExpression)
|
||||
assert.True(t, ok)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBindIndex(t *testing.T) {
|
||||
env := environment(map[string]interface{}{
|
||||
"a": StringType,
|
||||
"b": IntType,
|
||||
"c": NewOutputType(StringType),
|
||||
"d": NewOutputType(IntType),
|
||||
"e": NewPromiseType(StringType),
|
||||
"f": NewPromiseType(IntType),
|
||||
"g": NewListType(BoolType),
|
||||
"h": NewMapType(BoolType),
|
||||
"i": NewObjectType(map[string]Type{"foo": BoolType}),
|
||||
"j": NewOutputType(NewListType(BoolType)),
|
||||
"k": NewOutputType(NewMapType(BoolType)),
|
||||
"l": NewOutputType(NewObjectType(map[string]Type{"foo": BoolType})),
|
||||
"m": NewPromiseType(NewListType(BoolType)),
|
||||
"n": NewPromiseType(NewMapType(BoolType)),
|
||||
"o": NewPromiseType(NewObjectType(map[string]Type{"foo": BoolType})),
|
||||
})
|
||||
scope := env.scope()
|
||||
|
||||
cases := []exprTestCase{
|
||||
// Standard operations
|
||||
{x: "g[a]", t: BoolType},
|
||||
{x: "g[b]", t: BoolType},
|
||||
{x: "h[a]", t: BoolType},
|
||||
{x: "h[b]", t: BoolType},
|
||||
{x: "i[a]", t: BoolType},
|
||||
{x: "i[b]", t: BoolType},
|
||||
|
||||
// Lifted operations
|
||||
{x: "g[c]", t: NewOutputType(BoolType)},
|
||||
{x: "g[d]", t: NewOutputType(BoolType)},
|
||||
{x: "h[c]", t: NewOutputType(BoolType)},
|
||||
{x: "h[d]", t: NewOutputType(BoolType)},
|
||||
{x: "i[c]", t: NewOutputType(BoolType)},
|
||||
{x: "i[d]", t: NewOutputType(BoolType)},
|
||||
{x: "g[e]", t: NewPromiseType(BoolType)},
|
||||
{x: "g[f]", t: NewPromiseType(BoolType)},
|
||||
{x: "h[e]", t: NewPromiseType(BoolType)},
|
||||
{x: "h[f]", t: NewPromiseType(BoolType)},
|
||||
{x: "i[e]", t: NewPromiseType(BoolType)},
|
||||
{x: "i[f]", t: NewPromiseType(BoolType)},
|
||||
{x: "j[a]", t: NewOutputType(BoolType)},
|
||||
{x: "j[b]", t: NewOutputType(BoolType)},
|
||||
{x: "k[a]", t: NewOutputType(BoolType)},
|
||||
{x: "k[b]", t: NewOutputType(BoolType)},
|
||||
{x: "l[a]", t: NewOutputType(BoolType)},
|
||||
{x: "l[b]", t: NewOutputType(BoolType)},
|
||||
{x: "m[a]", t: NewPromiseType(BoolType)},
|
||||
{x: "m[b]", t: NewPromiseType(BoolType)},
|
||||
{x: "n[a]", t: NewPromiseType(BoolType)},
|
||||
{x: "n[b]", t: NewPromiseType(BoolType)},
|
||||
{x: "o[a]", t: NewPromiseType(BoolType)},
|
||||
{x: "o[b]", t: NewPromiseType(BoolType)},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.x, func(t *testing.T) {
|
||||
expr, diags := BindExpressionText(c.x, scope)
|
||||
assert.Len(t, diags, 0)
|
||||
assert.Equal(t, c.t, expr.Type())
|
||||
_, ok := expr.(*IndexExpression)
|
||||
assert.True(t, ok)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBindObjectCons(t *testing.T) {
|
||||
env := environment(map[string]interface{}{
|
||||
"a": StringType,
|
||||
"b": NumberType,
|
||||
"c": BoolType,
|
||||
"d": NewOutputType(StringType),
|
||||
"e": NewOutputType(NumberType),
|
||||
"f": NewOutputType(BoolType),
|
||||
"g": NewPromiseType(StringType),
|
||||
"h": NewPromiseType(NumberType),
|
||||
"i": NewPromiseType(BoolType),
|
||||
})
|
||||
scope := env.scope()
|
||||
|
||||
ot := NewObjectType(map[string]Type{"foo": StringType, "0": NumberType, "false": BoolType})
|
||||
mt := NewMapType(StringType)
|
||||
cases := []exprTestCase{
|
||||
// Standard operations
|
||||
{x: `{"foo": "oof", 0: 42, false: true}`, t: ot},
|
||||
{x: `{(a): a, (b): b, (c): c}`, t: mt},
|
||||
|
||||
// Lifted operations
|
||||
{x: `{(d): a, (b): b, (c): c}`, t: NewOutputType(mt)},
|
||||
{x: `{(a): a, (e): b, (c): c}`, t: NewOutputType(mt)},
|
||||
{x: `{(a): a, (b): b, (f): c}`, t: NewOutputType(mt)},
|
||||
{x: `{(g): a, (b): b, (c): c}`, t: NewPromiseType(mt)},
|
||||
{x: `{(a): a, (h): b, (c): c}`, t: NewPromiseType(mt)},
|
||||
{x: `{(a): a, (b): b, (i): c}`, t: NewPromiseType(mt)},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.x, func(t *testing.T) {
|
||||
expr, diags := BindExpressionText(c.x, scope)
|
||||
assert.Len(t, diags, 0)
|
||||
assert.Equal(t, c.t, expr.Type())
|
||||
_, ok := expr.(*ObjectConsExpression)
|
||||
assert.True(t, ok)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBindRelativeTraversal(t *testing.T) {
|
||||
env := environment(map[string]interface{}{
|
||||
"a": NewMapType(StringType),
|
||||
"aa": NewMapType(NewOutputType(StringType)),
|
||||
"b": NewOutputType(NewMapType(StringType)),
|
||||
"c": NewPromiseType(NewMapType(StringType)),
|
||||
"d": NewListType(StringType),
|
||||
"dd": NewListType(NewOutputType(StringType)),
|
||||
"e": NewOutputType(NewListType(StringType)),
|
||||
"f": NewPromiseType(NewListType(StringType)),
|
||||
"g": BoolType,
|
||||
"h": NewOutputType(BoolType),
|
||||
"i": NewPromiseType(BoolType),
|
||||
"j": StringType,
|
||||
"k": NewOutputType(StringType),
|
||||
"l": NewPromiseType(StringType),
|
||||
})
|
||||
scope := env.scope()
|
||||
|
||||
cases := []exprTestCase{
|
||||
// Object for
|
||||
{x: `{for k, v in {foo: "bar"}: k => v}.foo`, t: StringType},
|
||||
{x: `{for k, v in {foo: "bar"}: k => 0}.foo`, t: NumberType},
|
||||
{x: `{for k, v in {foo: 0}: k => v}.foo`, t: NumberType},
|
||||
{x: `{for k, v in a: k => v}.foo`, t: StringType},
|
||||
{x: `{for k, v in aa: k => v}.foo`, t: NewOutputType(StringType)},
|
||||
{x: `{for k, v in a: k => 0}.foo`, t: NumberType},
|
||||
{x: `{for k, v in d: v => k}.foo`, t: NumberType},
|
||||
{x: `{for k, v in d: v => k...}.foo[0]`, t: NumberType},
|
||||
{x: `{for k, v in d: v => k if k > 10}.foo`, t: NumberType},
|
||||
|
||||
// List for
|
||||
{x: `[for k, v in {}: [k, v]].0`, t: NewTupleType(StringType, NoneType)},
|
||||
{x: `[for k, _ in {}: k].0`, t: StringType},
|
||||
|
||||
// Lifted operations
|
||||
{x: `{for k, v in b: k => v}.foo`, t: NewOutputType(StringType)},
|
||||
{x: `{for k, v in c: k => v}.foo`, t: NewPromiseType(StringType)},
|
||||
{x: `[for v in e: v].foo`, t: NewOutputType(StringType)},
|
||||
{x: `[for v in f: v].foo`, t: NewPromiseType(StringType)},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.x, func(t *testing.T) {
|
||||
expr, diags := BindExpressionText(c.x, scope)
|
||||
assert.Len(t, diags, 0)
|
||||
assert.Equal(t, c.t, expr.Type())
|
||||
_, ok := expr.(*RelativeTraversalExpression)
|
||||
assert.True(t, ok)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBindScopeTraversal(t *testing.T) {
|
||||
ot := NewObjectType(map[string]Type{
|
||||
"foo": NewListType(StringType),
|
||||
"bar": NewObjectType(map[string]Type{
|
||||
"baz": StringType,
|
||||
}),
|
||||
})
|
||||
env := environment(map[string]interface{}{
|
||||
"a": StringType,
|
||||
"b": IntType,
|
||||
"c": NewListType(BoolType),
|
||||
"d": NewMapType(BoolType),
|
||||
"e": ot,
|
||||
"f": NewOutputType(StringType),
|
||||
"g": NewOutputType(IntType),
|
||||
"h": NewOutputType(NewListType(BoolType)),
|
||||
"i": NewOutputType(NewMapType(BoolType)),
|
||||
"j": NewOutputType(ot),
|
||||
"k": NewPromiseType(StringType),
|
||||
"l": NewPromiseType(IntType),
|
||||
"m": NewPromiseType(NewListType(BoolType)),
|
||||
"n": NewPromiseType(NewMapType(BoolType)),
|
||||
"o": NewPromiseType(ot),
|
||||
})
|
||||
scope := env.scope()
|
||||
|
||||
cases := []exprTestCase{
|
||||
// Standard traversals
|
||||
{x: `a`, t: StringType},
|
||||
{x: `b`, t: IntType},
|
||||
{x: `c`, t: NewListType(BoolType)},
|
||||
{x: `d`, t: NewMapType(BoolType)},
|
||||
{x: `e`, t: ot},
|
||||
{x: `f`, t: NewOutputType(StringType)},
|
||||
{x: `g`, t: NewOutputType(IntType)},
|
||||
{x: `k`, t: NewPromiseType(StringType)},
|
||||
{x: `l`, t: NewPromiseType(IntType)},
|
||||
{x: `c.0`, t: BoolType},
|
||||
{x: `d.foo`, t: BoolType},
|
||||
{x: `e.foo`, t: NewListType(StringType)},
|
||||
{x: `e.foo.0`, t: StringType},
|
||||
{x: `e.bar`, t: ot.Properties["bar"]},
|
||||
{x: `e.bar.baz`, t: StringType},
|
||||
|
||||
// Lifted traversals
|
||||
{x: `h.0`, t: NewOutputType(BoolType)},
|
||||
{x: `i.foo`, t: NewOutputType(BoolType)},
|
||||
{x: `j.foo`, t: NewOutputType(NewListType(StringType))},
|
||||
{x: `j.foo.0`, t: NewOutputType(StringType)},
|
||||
{x: `j.bar`, t: NewOutputType(ot.Properties["bar"])},
|
||||
{x: `j.bar.baz`, t: NewOutputType(StringType)},
|
||||
{x: `m.0`, t: NewPromiseType(BoolType)},
|
||||
{x: `n.foo`, t: NewPromiseType(BoolType)},
|
||||
{x: `o.foo`, t: NewPromiseType(NewListType(StringType))},
|
||||
{x: `o.foo.0`, t: NewPromiseType(StringType)},
|
||||
{x: `o.bar`, t: NewPromiseType(ot.Properties["bar"])},
|
||||
{x: `o.bar.baz`, t: NewPromiseType(StringType)},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.x, func(t *testing.T) {
|
||||
expr, diags := BindExpressionText(c.x, scope)
|
||||
assert.Len(t, diags, 0)
|
||||
assert.Equal(t, c.t, expr.Type())
|
||||
_, ok := expr.(*ScopeTraversalExpression)
|
||||
assert.True(t, ok)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBindSplat(t *testing.T) {
|
||||
ot := NewObjectType(map[string]Type{
|
||||
"foo": NewListType(StringType),
|
||||
"bar": NewObjectType(map[string]Type{
|
||||
"baz": StringType,
|
||||
}),
|
||||
})
|
||||
env := environment(map[string]interface{}{
|
||||
"a": NewListType(NewListType(StringType)),
|
||||
"b": NewListType(ot),
|
||||
"c": NewSetType(NewListType(StringType)),
|
||||
"d": NewSetType(ot),
|
||||
// "e": NewTupleType(NewListType(StringType)),
|
||||
// "f": NewTupleType(ot),
|
||||
"g": NewListType(NewListType(NewOutputType(StringType))),
|
||||
"h": NewListType(NewListType(NewPromiseType(StringType))),
|
||||
// "i": NewSetType(NewListType(NewOutputType(StringType))),
|
||||
// "j": NewSetType(NewListType(NewPromiseType(StringType))),
|
||||
// "k": NewTupleType(NewListType(NewOutputType(StringType))),
|
||||
// "l": NewTupleType(NewListType(NewPromiseType(StringType))),
|
||||
"m": NewOutputType(NewListType(ot)),
|
||||
"n": NewPromiseType(NewListType(ot)),
|
||||
// "o": NewOutputType(NewSetType(ot)),
|
||||
// "p": NewPromiseType(NewSetType(ot)),
|
||||
// "q": NewOutputType(NewTupleType(ot)),
|
||||
// "r": NewPromiseType(NewTupleType(ot)),
|
||||
})
|
||||
scope := env.scope()
|
||||
|
||||
cases := []exprTestCase{
|
||||
// Standard operations
|
||||
{x: `a[*][0]`, t: NewListType(StringType)},
|
||||
{x: `b[*].bar.baz`, t: NewListType(StringType)},
|
||||
// {x: `c[*][0]`, t: NewSetType(StringType)},
|
||||
// {x: `d[*].bar.baz`, t: NewSetType(StringType)},
|
||||
// {x: `e[*][0]`, t: NewTupleType(StringType)},
|
||||
// {x: `f[*].bar.baz`, t: NewTupleType(StringType)},
|
||||
{x: `g[*][0]`, t: NewListType(NewOutputType(StringType))},
|
||||
{x: `h[*][0]`, t: NewListType(NewPromiseType(StringType))},
|
||||
// {x: `i[*][0]`, t: NewListType(NewOutputType(StringType))},
|
||||
// {x: `j[*][0]`, t: NewListType(NewPromiseType(StringType))},
|
||||
// {x: `k[*][0]`, t: NewTupleType(NewOutputType(StringType))},
|
||||
// {x: `l[*][0]`, t: NewTupleType(NewPromiseType(StringType))},
|
||||
|
||||
// Lifted operations
|
||||
{x: `m[*].bar.baz`, t: NewOutputType(NewListType(StringType))},
|
||||
{x: `n[*].bar.baz`, t: NewPromiseType(NewListType(StringType))},
|
||||
// {x: `o[*].bar.baz`, t: NewOutputType(NewListType(StringType))},
|
||||
// {x: `p[*].bar.baz`, t: NewPromiseType(NewListType(StringType))},
|
||||
// {x: `q[*].bar.baz`, t: NewOutputType(NewTupleType(StringType))},
|
||||
// {x: `r[*].bar.baz`, t: NewPromiseType(NewTupleType(StringType))},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.x, func(t *testing.T) {
|
||||
expr, diags := BindExpressionText(c.x, scope)
|
||||
assert.Len(t, diags, 0)
|
||||
assert.Equal(t, c.t, expr.Type())
|
||||
_, ok := expr.(*SplatExpression)
|
||||
assert.True(t, ok)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBindTemplate(t *testing.T) {
|
||||
env := environment(map[string]interface{}{
|
||||
"a": StringType,
|
||||
"b": NumberType,
|
||||
"c": BoolType,
|
||||
"d": NewListType(StringType),
|
||||
"e": NewOutputType(StringType),
|
||||
"f": NewOutputType(NumberType),
|
||||
"g": NewOutputType(BoolType),
|
||||
"h": NewOutputType(NewListType(StringType)),
|
||||
"i": NewPromiseType(StringType),
|
||||
"j": NewPromiseType(NumberType),
|
||||
"k": NewPromiseType(BoolType),
|
||||
"l": NewPromiseType(NewListType(StringType)),
|
||||
})
|
||||
scope := env.scope()
|
||||
|
||||
cases := []exprTestCase{
|
||||
// Unwrapped interpolations
|
||||
{x: `"foo"`, t: StringType, xt: &LiteralValueExpression{}},
|
||||
{x: `"${"foo"}"`, t: StringType, xt: &LiteralValueExpression{}},
|
||||
{x: `"${0}"`, t: NumberType, xt: &LiteralValueExpression{}},
|
||||
{x: `"${true}"`, t: BoolType, xt: &LiteralValueExpression{}},
|
||||
{x: `"${d}"`, t: NewListType(StringType), xt: &ScopeTraversalExpression{}},
|
||||
{x: `"${e}"`, t: NewOutputType(StringType), xt: &ScopeTraversalExpression{}},
|
||||
{x: `"${i}"`, t: NewPromiseType(StringType), xt: &ScopeTraversalExpression{}},
|
||||
|
||||
// Simple interpolations
|
||||
{x: `"v: ${a}"`, t: StringType},
|
||||
{x: `"v: ${b}"`, t: StringType},
|
||||
{x: `"v: ${c}"`, t: StringType},
|
||||
{x: `"v: ${d}"`, t: StringType},
|
||||
|
||||
// Template control expressions
|
||||
{x: `"%{if c} v: ${a} %{endif}"`, t: StringType},
|
||||
{x: `"%{for v in d} v: ${v} %{endfor}"`, t: StringType},
|
||||
|
||||
// Lifted operations
|
||||
{x: `"v: ${e}"`, t: NewOutputType(StringType)},
|
||||
{x: `"v: ${f}"`, t: NewOutputType(StringType)},
|
||||
{x: `"v: ${g}"`, t: NewOutputType(StringType)},
|
||||
{x: `"v: ${h}"`, t: NewOutputType(StringType)},
|
||||
{x: `"%{if g} v: ${a} %{endif}"`, t: NewOutputType(StringType)},
|
||||
{x: `"%{for v in h} v: ${v} %{endfor}"`, t: NewOutputType(StringType)},
|
||||
{x: `"v: ${i}"`, t: NewPromiseType(StringType)},
|
||||
{x: `"v: ${j}"`, t: NewPromiseType(StringType)},
|
||||
{x: `"v: ${k}"`, t: NewPromiseType(StringType)},
|
||||
{x: `"v: ${l}"`, t: NewPromiseType(StringType)},
|
||||
{x: `"%{if k} v: ${a} %{endif}"`, t: NewPromiseType(StringType)},
|
||||
{x: `"%{for v in l} v: ${v} %{endfor}"`, t: NewPromiseType(StringType)},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.x, func(t *testing.T) {
|
||||
expr, diags := BindExpressionText(c.x, scope)
|
||||
assert.Len(t, diags, 0)
|
||||
assert.Equal(t, c.t, expr.Type())
|
||||
|
||||
var ok bool
|
||||
switch c.xt.(type) {
|
||||
case *LiteralValueExpression:
|
||||
_, ok = expr.(*LiteralValueExpression)
|
||||
case *ScopeTraversalExpression:
|
||||
_, ok = expr.(*ScopeTraversalExpression)
|
||||
default:
|
||||
_, ok = expr.(*TemplateExpression)
|
||||
}
|
||||
assert.True(t, ok)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBindTupleCons(t *testing.T) {
|
||||
env := environment(map[string]interface{}{
|
||||
"a": NewOutputType(StringType),
|
||||
"b": NewPromiseType(StringType),
|
||||
"c": NewUnionType(StringType, BoolType),
|
||||
})
|
||||
scope := env.scope()
|
||||
|
||||
cases := []exprTestCase{
|
||||
{x: `["foo", "bar", "baz"]`, t: NewTupleType(StringType, StringType, StringType)},
|
||||
{x: `[0, "foo", true]`, t: NewTupleType(NumberType, StringType, BoolType)},
|
||||
{x: `[a, b, c]`, t: NewTupleType(env["a"].(Type), env["b"].(Type), env["c"].(Type))},
|
||||
{x: `[{"foo": "bar"}]`, t: NewTupleType(NewObjectType(map[string]Type{"foo": StringType}))},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.x, func(t *testing.T) {
|
||||
expr, diags := BindExpressionText(c.x, scope)
|
||||
assert.Len(t, diags, 0)
|
||||
assert.Equal(t, c.t, expr.Type())
|
||||
_, ok := expr.(*TupleConsExpression)
|
||||
assert.True(t, ok)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBindUnaryOp(t *testing.T) {
|
||||
env := environment(map[string]interface{}{
|
||||
"a": NumberType,
|
||||
"b": BoolType,
|
||||
"c": NewOutputType(NumberType),
|
||||
"d": NewOutputType(BoolType),
|
||||
"e": NewPromiseType(NumberType),
|
||||
"f": NewPromiseType(BoolType),
|
||||
})
|
||||
scope := env.scope()
|
||||
|
||||
cases := []exprTestCase{
|
||||
// Standard operations
|
||||
{x: `-a`, t: NumberType},
|
||||
{x: `!b`, t: BoolType},
|
||||
|
||||
// Lifted operations
|
||||
{x: `-c`, t: NewOutputType(NumberType)},
|
||||
{x: `-e`, t: NewPromiseType(NumberType)},
|
||||
{x: `!d`, t: NewOutputType(BoolType)},
|
||||
{x: `!f`, t: NewPromiseType(BoolType)},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.x, func(t *testing.T) {
|
||||
expr, diags := BindExpressionText(c.x, scope)
|
||||
assert.Len(t, diags, 0)
|
||||
assert.Equal(t, c.t, expr.Type())
|
||||
_, ok := expr.(*UnaryOpExpression)
|
||||
assert.True(t, ok)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
57
pkg/codegen/hcl2/model/block.go
Normal file
57
pkg/codegen/hcl2/model/block.go
Normal file
|
@ -0,0 +1,57 @@
|
|||
// 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 model
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||
"github.com/pulumi/pulumi/pkg/codegen/hcl2/syntax"
|
||||
)
|
||||
|
||||
// Block represents an HCL2 block.
|
||||
type Block struct {
|
||||
// The syntax node for the block, if any.
|
||||
Syntax *hclsyntax.Block
|
||||
// The tokens for the block.
|
||||
Tokens syntax.BlockTokens
|
||||
|
||||
// The block's type.
|
||||
Type string
|
||||
// The block's labels.
|
||||
Labels []string
|
||||
|
||||
// The block's body.
|
||||
Body *Body
|
||||
}
|
||||
|
||||
// SyntaxNode returns the syntax node of the block, and will either return an *hclsyntax.Block or syntax.None.
|
||||
func (b *Block) SyntaxNode() hclsyntax.Node {
|
||||
return syntaxOrNone(b.Syntax)
|
||||
}
|
||||
|
||||
func (*Block) isBodyItem() {}
|
||||
|
||||
// BindBlock binds an HCL2 block using the given scopes and token map.
|
||||
func BindBlock(block *hclsyntax.Block, scopes Scopes, tokens syntax.TokenMap) (*Block, hcl.Diagnostics) {
|
||||
body, diagnostics := BindBody(block.Body, scopes, tokens)
|
||||
blockTokens, _ := tokens.ForNode(block).(syntax.BlockTokens)
|
||||
return &Block{
|
||||
Syntax: block,
|
||||
Tokens: blockTokens,
|
||||
Type: block.Type,
|
||||
Labels: block.Labels,
|
||||
Body: body,
|
||||
}, diagnostics
|
||||
}
|
79
pkg/codegen/hcl2/model/body.go
Normal file
79
pkg/codegen/hcl2/model/body.go
Normal file
|
@ -0,0 +1,79 @@
|
|||
// 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 model
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||
"github.com/pulumi/pulumi/pkg/codegen/hcl2/syntax"
|
||||
"github.com/pulumi/pulumi/sdk/go/common/util/contract"
|
||||
)
|
||||
|
||||
// BodyItem represents either an *Attribute or a *Block that is part of an HCL2 Body.
|
||||
type BodyItem interface {
|
||||
// SyntaxNode returns syntax node of the item.
|
||||
SyntaxNode() hclsyntax.Node
|
||||
|
||||
isBodyItem()
|
||||
}
|
||||
|
||||
// Body represents an HCL2 body. A Body may be the root of an HCL2 file or the contents of an HCL2 block.
|
||||
type Body struct {
|
||||
// The syntax node for the body, if any.
|
||||
Syntax *hclsyntax.Body
|
||||
// The tokens for the body.
|
||||
Tokens syntax.BodyTokens
|
||||
|
||||
// The items that make up the body's contents.
|
||||
Items []BodyItem
|
||||
}
|
||||
|
||||
// SyntaxNode returns the syntax node of the body, and will either return an *hclsyntax.Body or syntax.None.
|
||||
func (b *Body) SyntaxNode() hclsyntax.Node {
|
||||
return syntaxOrNone(b.Syntax)
|
||||
}
|
||||
|
||||
// BindBody binds an HCL2 body using the given scopes and token map.
|
||||
func BindBody(body *hclsyntax.Body, scopes Scopes, tokens syntax.TokenMap) (*Body, hcl.Diagnostics) {
|
||||
var diagnostics hcl.Diagnostics
|
||||
|
||||
syntaxItems := SourceOrderBody(body)
|
||||
items := make([]BodyItem, len(syntaxItems))
|
||||
for i, syntaxItem := range syntaxItems {
|
||||
var itemDiags hcl.Diagnostics
|
||||
switch syntaxItem := syntaxItem.(type) {
|
||||
case *hclsyntax.Attribute:
|
||||
scope, scopeDiags := scopes.GetScopeForAttribute(syntaxItem)
|
||||
diagnostics = append(diagnostics, scopeDiags...)
|
||||
|
||||
items[i], itemDiags = BindAttribute(syntaxItem, scope, tokens)
|
||||
case *hclsyntax.Block:
|
||||
scopes, scopesDiags := scopes.GetScopesForBlock(syntaxItem)
|
||||
diagnostics = append(diagnostics, scopesDiags...)
|
||||
|
||||
items[i], itemDiags = BindBlock(syntaxItem, scopes, tokens)
|
||||
default:
|
||||
contract.Failf("unexpected syntax item of type %T (%v)", syntaxItem, syntaxItem.Range())
|
||||
}
|
||||
diagnostics = append(diagnostics, itemDiags...)
|
||||
}
|
||||
|
||||
bodyTokens, _ := tokens.ForNode(body).(syntax.BodyTokens)
|
||||
return &Body{
|
||||
Syntax: body,
|
||||
Tokens: bodyTokens,
|
||||
Items: items,
|
||||
}, diagnostics
|
||||
}
|
114
pkg/codegen/hcl2/model/diagnostics.go
Normal file
114
pkg/codegen/hcl2/model/diagnostics.go
Normal file
|
@ -0,0 +1,114 @@
|
|||
// 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 model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||
)
|
||||
|
||||
func errorf(subject hcl.Range, f string, args ...interface{}) *hcl.Diagnostic {
|
||||
return diagf(hcl.DiagError, subject, f, args...)
|
||||
}
|
||||
|
||||
func diagf(severity hcl.DiagnosticSeverity, subject hcl.Range, f string, args ...interface{}) *hcl.Diagnostic {
|
||||
message := fmt.Sprintf(f, args...)
|
||||
return &hcl.Diagnostic{
|
||||
Severity: severity,
|
||||
Summary: message,
|
||||
Detail: message,
|
||||
Subject: &subject,
|
||||
}
|
||||
}
|
||||
|
||||
func ExprNotConvertible(destType Type, expr Expression) *hcl.Diagnostic {
|
||||
return errorf(expr.SyntaxNode().Range(), "cannot assign expression of type %v to location of type %v", expr.Type(),
|
||||
destType)
|
||||
}
|
||||
|
||||
func objectKeysMustBeStrings(expr Expression) *hcl.Diagnostic {
|
||||
return errorf(expr.SyntaxNode().Range(),
|
||||
"object keys must be strings: cannot assign expression of type %v to location of type string", expr.Type())
|
||||
}
|
||||
|
||||
func unsupportedLiteralValue(syntax *hclsyntax.LiteralValueExpr) *hcl.Diagnostic {
|
||||
return errorf(syntax.Range(), "unsupported literal value of type %v", syntax.Val.Type())
|
||||
}
|
||||
|
||||
func unknownFunction(name string, nameRange hcl.Range) *hcl.Diagnostic {
|
||||
return errorf(nameRange, "unknown function '%s'", name)
|
||||
}
|
||||
|
||||
func missingRequiredArgument(param Parameter, callRange hcl.Range) *hcl.Diagnostic {
|
||||
return errorf(callRange, "missing required parameter '%s'", param.Name)
|
||||
}
|
||||
|
||||
func extraArguments(expected, actual int, callRange hcl.Range) *hcl.Diagnostic {
|
||||
return errorf(callRange, "too many arguments to call: expected %v, got %v", expected, actual)
|
||||
}
|
||||
|
||||
func unsupportedMapKey(keyRange hcl.Range) *hcl.Diagnostic {
|
||||
return errorf(keyRange, "map keys must be strings")
|
||||
}
|
||||
|
||||
func unsupportedListIndex(indexRange hcl.Range) *hcl.Diagnostic {
|
||||
return errorf(indexRange, "list indices must be numbers")
|
||||
}
|
||||
|
||||
func unsupportedTupleIndex(indexRange hcl.Range) *hcl.Diagnostic {
|
||||
return errorf(indexRange, "tuple indices must be integers")
|
||||
}
|
||||
|
||||
func unsupportedObjectProperty(indexRange hcl.Range) *hcl.Diagnostic {
|
||||
return errorf(indexRange, "object properties must be strings")
|
||||
}
|
||||
|
||||
func tupleIndexOutOfRange(tupleLen int, indexRange hcl.Range) *hcl.Diagnostic {
|
||||
return errorf(indexRange, "tuple index must be between 0 and %d", tupleLen)
|
||||
}
|
||||
|
||||
func unknownObjectProperty(name string, indexRange hcl.Range) *hcl.Diagnostic {
|
||||
return errorf(indexRange, "unknown property '%s'", name)
|
||||
}
|
||||
|
||||
func unsupportedReceiverType(receiver Type, indexRange hcl.Range) *hcl.Diagnostic {
|
||||
return errorf(indexRange, "cannot traverse value of type %v", receiver)
|
||||
}
|
||||
|
||||
func unsupportedCollectionType(collectionType Type, iteratorRange hcl.Range) *hcl.Diagnostic {
|
||||
return errorf(iteratorRange, "cannot iterator over a value of type %v", collectionType)
|
||||
}
|
||||
|
||||
func undefinedVariable(variableRange hcl.Range) *hcl.Diagnostic {
|
||||
return errorf(variableRange, "undefined variable")
|
||||
}
|
||||
|
||||
func internalError(rng hcl.Range, fmt string, args ...interface{}) *hcl.Diagnostic {
|
||||
return errorf(rng, "Internal error: "+fmt, args...)
|
||||
}
|
||||
|
||||
func nameAlreadyDefined(name string, rng hcl.Range) *hcl.Diagnostic {
|
||||
return errorf(rng, "name %v already defined", name)
|
||||
}
|
||||
|
||||
func cannotTraverseKeyword(name string, rng hcl.Range) *hcl.Diagnostic {
|
||||
return errorf(rng, "'%s' is a keyword and cannot be traversed", name)
|
||||
}
|
||||
|
||||
func cannotTraverseFunction(rng hcl.Range) *hcl.Diagnostic {
|
||||
return errorf(rng, "functions cannot be traversed")
|
||||
}
|
428
pkg/codegen/hcl2/model/expression.go
Normal file
428
pkg/codegen/hcl2/model/expression.go
Normal file
|
@ -0,0 +1,428 @@
|
|||
// 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 model
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
// Expression represents a semantically-analyzed HCL2 expression.
|
||||
type Expression interface {
|
||||
// SyntaxNode returns the hclsyntax.Node associated with the expression.
|
||||
SyntaxNode() hclsyntax.Node
|
||||
// Type returns the type of the expression.
|
||||
Type() Type
|
||||
|
||||
isExpression()
|
||||
}
|
||||
|
||||
// AnonymousFunctionExpression represents a semantically-analyzed anonymous function expression.
|
||||
//
|
||||
// These expressions are not the result of semantically analyzing syntax nodes. Instead, they may be synthesized by
|
||||
// transforms over the IR for a program (e.g. the Apply transform).
|
||||
type AnonymousFunctionExpression struct {
|
||||
// The signature for the anonymous function.
|
||||
Signature StaticFunctionSignature
|
||||
// The parameter definitions for the anonymous function.
|
||||
Parameters []*Variable
|
||||
|
||||
// The body of the anonymous function.
|
||||
Body Expression
|
||||
}
|
||||
|
||||
// SyntaxNode returns the syntax node associated with the body of the anonymous function.
|
||||
func (x *AnonymousFunctionExpression) SyntaxNode() hclsyntax.Node {
|
||||
return x.Body.SyntaxNode()
|
||||
}
|
||||
|
||||
// Type returns the type of the anonymous function expression.
|
||||
//
|
||||
// TODO: currently this returns the any type. Instead, it should return a function type.
|
||||
func (x *AnonymousFunctionExpression) Type() Type {
|
||||
return DynamicType
|
||||
}
|
||||
|
||||
func (*AnonymousFunctionExpression) isExpression() {}
|
||||
|
||||
// BinaryOpExpression represents a semantically-analyzed binary operation.
|
||||
type BinaryOpExpression struct {
|
||||
// The syntax node associated with the binary operation.
|
||||
Syntax *hclsyntax.BinaryOpExpr
|
||||
|
||||
// The left-hand operand of the operation.
|
||||
LeftOperand Expression
|
||||
// The right-hand operand of the operation.
|
||||
RightOperand Expression
|
||||
|
||||
exprType Type
|
||||
}
|
||||
|
||||
// SyntaxNode returns the syntax node associated with the binary operation.
|
||||
func (x *BinaryOpExpression) SyntaxNode() hclsyntax.Node {
|
||||
return x.Syntax
|
||||
}
|
||||
|
||||
// Type returns the type of the binary operation.
|
||||
func (x *BinaryOpExpression) Type() Type {
|
||||
return x.exprType
|
||||
}
|
||||
|
||||
func (*BinaryOpExpression) isExpression() {}
|
||||
|
||||
// ConditionalExpression represents a semantically-analzed conditional expression (i.e.
|
||||
// <condition> '?' <true> ':' <false>).
|
||||
type ConditionalExpression struct {
|
||||
// The syntax node associated with the conditional expression.
|
||||
Syntax *hclsyntax.ConditionalExpr
|
||||
|
||||
// The condition.
|
||||
Condition Expression
|
||||
// The result of the expression if the condition evaluates to true.
|
||||
TrueResult Expression
|
||||
// The result of the expression if the condition evaluates to false.
|
||||
FalseResult Expression
|
||||
|
||||
exprType Type
|
||||
}
|
||||
|
||||
// SyntaxNode returns the syntax node associated with the conditional expression.
|
||||
func (x *ConditionalExpression) SyntaxNode() hclsyntax.Node {
|
||||
return x.Syntax
|
||||
}
|
||||
|
||||
// Type returns the type of the conditional expression.
|
||||
func (x *ConditionalExpression) Type() Type {
|
||||
return x.exprType
|
||||
}
|
||||
|
||||
func (*ConditionalExpression) isExpression() {}
|
||||
|
||||
// ErrorExpression represents an expression that could not be bound due to an error.
|
||||
type ErrorExpression struct {
|
||||
// The syntax node associated with the error,
|
||||
Syntax hclsyntax.Node
|
||||
|
||||
exprType Type
|
||||
}
|
||||
|
||||
// SyntaxNode returns the syntax node associated with the error expression.
|
||||
func (x *ErrorExpression) SyntaxNode() hclsyntax.Node {
|
||||
return x.Syntax
|
||||
}
|
||||
|
||||
// Type returns the type of the error expression.
|
||||
func (x *ErrorExpression) Type() Type {
|
||||
return x.exprType
|
||||
}
|
||||
|
||||
func (*ErrorExpression) isExpression() {}
|
||||
|
||||
// ForExpression represents a semantically-analyzed for expression.
|
||||
type ForExpression struct {
|
||||
// The syntax node associated with the for expression.
|
||||
Syntax *hclsyntax.ForExpr
|
||||
|
||||
// The collection being iterated.
|
||||
Collection Expression
|
||||
// The expression that generates the keys of the result, if any. If this field is non-nil, the result is a map.
|
||||
Key Expression
|
||||
// The expression that generates the values of the result.
|
||||
Value Expression
|
||||
// The condition that filters the items of the result, if any.
|
||||
Condition Expression
|
||||
|
||||
exprType Type
|
||||
}
|
||||
|
||||
// SyntaxNode returns the syntax node associated with the for expression.
|
||||
func (x *ForExpression) SyntaxNode() hclsyntax.Node {
|
||||
return x.Syntax
|
||||
}
|
||||
|
||||
// Type returns the type of the for expression.
|
||||
func (x *ForExpression) Type() Type {
|
||||
return x.exprType
|
||||
}
|
||||
|
||||
func (*ForExpression) isExpression() {}
|
||||
|
||||
// FunctionCallExpression represents a semantically-analyzed function call expression.
|
||||
type FunctionCallExpression struct {
|
||||
// The syntax node associated with the function call expression.
|
||||
Syntax *hclsyntax.FunctionCallExpr
|
||||
|
||||
// The name of the called function.
|
||||
Name string
|
||||
// The signature of the called function.
|
||||
Signature StaticFunctionSignature
|
||||
// The arguments to the function call.
|
||||
Args []Expression
|
||||
}
|
||||
|
||||
// SyntaxNode returns the syntax node associated with the function call expression.
|
||||
func (x *FunctionCallExpression) SyntaxNode() hclsyntax.Node {
|
||||
return x.Syntax
|
||||
}
|
||||
|
||||
// Type returns the type of the function call expression.
|
||||
func (x *FunctionCallExpression) Type() Type {
|
||||
return x.Signature.ReturnType
|
||||
}
|
||||
|
||||
func (*FunctionCallExpression) isExpression() {}
|
||||
|
||||
// IndexExpression represents a semantically-analyzed index expression.
|
||||
type IndexExpression struct {
|
||||
// The syntax node associated with the index expression.
|
||||
Syntax *hclsyntax.IndexExpr
|
||||
|
||||
// The collection being indexed.
|
||||
Collection Expression
|
||||
// The index key.
|
||||
Key Expression
|
||||
|
||||
exprType Type
|
||||
}
|
||||
|
||||
// SyntaxNode returns the syntax node associated with the index expression.
|
||||
func (x *IndexExpression) SyntaxNode() hclsyntax.Node {
|
||||
return x.Syntax
|
||||
}
|
||||
|
||||
// Type returns the type of the index expression.
|
||||
func (x *IndexExpression) Type() Type {
|
||||
return x.exprType
|
||||
}
|
||||
|
||||
func (*IndexExpression) isExpression() {}
|
||||
|
||||
// LiteralValueExpression represents a semantically-analyzed literal value expression.
|
||||
type LiteralValueExpression struct {
|
||||
// The syntax node associated with the literal value expression.
|
||||
Syntax *hclsyntax.LiteralValueExpr
|
||||
|
||||
// The value of the expression.
|
||||
Value cty.Value
|
||||
|
||||
exprType Type
|
||||
}
|
||||
|
||||
// SyntaxNode returns the syntax node associated with the literal value expression.
|
||||
func (x *LiteralValueExpression) SyntaxNode() hclsyntax.Node {
|
||||
return x.Syntax
|
||||
}
|
||||
|
||||
// Type returns the type of the literal value expression.
|
||||
func (x *LiteralValueExpression) Type() Type {
|
||||
return x.exprType
|
||||
}
|
||||
|
||||
func (*LiteralValueExpression) isExpression() {}
|
||||
|
||||
// ObjectConsExpression represents a semantically-analyzed object construction expression.
|
||||
type ObjectConsExpression struct {
|
||||
// The syntax node associated with the object construction expression.
|
||||
Syntax *hclsyntax.ObjectConsExpr
|
||||
|
||||
// The items that comprise the object construction expression.
|
||||
Items []ObjectConsItem
|
||||
|
||||
exprType Type
|
||||
}
|
||||
|
||||
// SyntaxNode returns the syntax node associated with the object construction expression.
|
||||
func (x *ObjectConsExpression) SyntaxNode() hclsyntax.Node {
|
||||
return x.Syntax
|
||||
}
|
||||
|
||||
// Type returns the type of the object construction expression.
|
||||
func (x *ObjectConsExpression) Type() Type {
|
||||
return x.exprType
|
||||
}
|
||||
|
||||
// ObjectConsItem records a key-value pair that is part of object construction expression.
|
||||
type ObjectConsItem struct {
|
||||
// The key.
|
||||
Key Expression
|
||||
// The value.
|
||||
Value Expression
|
||||
}
|
||||
|
||||
func (*ObjectConsExpression) isExpression() {}
|
||||
|
||||
// RelativeTraversalExpression represents a semantically-analyzed relative traversal expression.
|
||||
type RelativeTraversalExpression struct {
|
||||
// The syntax node associated with the relative traversal expression.
|
||||
Syntax *hclsyntax.RelativeTraversalExpr
|
||||
|
||||
// The expression that computes the value being traversed.
|
||||
Source Expression
|
||||
// The traversal's parts.
|
||||
Parts []Traversable
|
||||
}
|
||||
|
||||
// SyntaxNode returns the syntax node associated with the relative traversal expression.
|
||||
func (x *RelativeTraversalExpression) SyntaxNode() hclsyntax.Node {
|
||||
return x.Syntax
|
||||
}
|
||||
|
||||
// Type returns the type of the relative traversal expression.
|
||||
func (x *RelativeTraversalExpression) Type() Type {
|
||||
return GetTraversableType(x.Parts[len(x.Parts)-1])
|
||||
}
|
||||
|
||||
func (*RelativeTraversalExpression) isExpression() {}
|
||||
|
||||
// ScopeTraversalExpression represents a semantically-analyzed scope traversal expression.
|
||||
type ScopeTraversalExpression struct {
|
||||
// The syntax node associated with the scope traversal expression.
|
||||
Syntax *hclsyntax.ScopeTraversalExpr
|
||||
|
||||
// The traversal's parts.
|
||||
Parts []Traversable
|
||||
}
|
||||
|
||||
// SyntaxNode returns the syntax node associated with the scope traversal expression.
|
||||
func (x *ScopeTraversalExpression) SyntaxNode() hclsyntax.Node {
|
||||
return x.Syntax
|
||||
}
|
||||
|
||||
// Type returns the type of the scope traversal expression.
|
||||
func (x *ScopeTraversalExpression) Type() Type {
|
||||
return GetTraversableType(x.Parts[len(x.Parts)-1])
|
||||
}
|
||||
|
||||
func (*ScopeTraversalExpression) isExpression() {}
|
||||
|
||||
// SplatExpression represents a semantically-analyzed splat expression.
|
||||
type SplatExpression struct {
|
||||
// The syntax node associated with the splat expression.
|
||||
Syntax *hclsyntax.SplatExpr
|
||||
|
||||
// The expression being splatted.
|
||||
Source Expression
|
||||
// The expression applied to each element of the splat.
|
||||
Each Expression
|
||||
// The local variable definition associated with the current item being processed. This definition is not part of
|
||||
// a scope, and can only be referenced by an AnonSymbolExpr.
|
||||
Item *Variable
|
||||
|
||||
exprType Type
|
||||
}
|
||||
|
||||
// SyntaxNode returns the syntax node associated with the splat expression.
|
||||
func (x *SplatExpression) SyntaxNode() hclsyntax.Node {
|
||||
return x.Syntax
|
||||
}
|
||||
|
||||
// Type returns the type of the splat expression.
|
||||
func (x *SplatExpression) Type() Type {
|
||||
return x.exprType
|
||||
}
|
||||
|
||||
func (*SplatExpression) isExpression() {}
|
||||
|
||||
// TemplateExpression represents a semantically-analyzed template expression.
|
||||
type TemplateExpression struct {
|
||||
// The syntax node associated with the template expression.
|
||||
Syntax *hclsyntax.TemplateExpr
|
||||
|
||||
// The parts of the template expression.
|
||||
Parts []Expression
|
||||
|
||||
exprType Type
|
||||
}
|
||||
|
||||
// SyntaxNode returns the syntax node associated with the template expression.
|
||||
func (x *TemplateExpression) SyntaxNode() hclsyntax.Node {
|
||||
return x.Syntax
|
||||
}
|
||||
|
||||
// Type returns the type of the template expression.
|
||||
func (x *TemplateExpression) Type() Type {
|
||||
return x.exprType
|
||||
}
|
||||
|
||||
func (*TemplateExpression) isExpression() {}
|
||||
|
||||
// TemplateJoinExpression represents a semantically-analyzed template join expression.
|
||||
type TemplateJoinExpression struct {
|
||||
// The syntax node associated with the template join expression.
|
||||
Syntax *hclsyntax.TemplateJoinExpr
|
||||
|
||||
// The tuple being joined.
|
||||
Tuple Expression
|
||||
|
||||
exprType Type
|
||||
}
|
||||
|
||||
// SyntaxNode returns the syntax node associated with the template join expression.
|
||||
func (x *TemplateJoinExpression) SyntaxNode() hclsyntax.Node {
|
||||
return x.Syntax
|
||||
}
|
||||
|
||||
// Type returns the type of the template join expression.
|
||||
func (x *TemplateJoinExpression) Type() Type {
|
||||
return x.exprType
|
||||
}
|
||||
|
||||
func (*TemplateJoinExpression) isExpression() {}
|
||||
|
||||
// TupleConsExpression represents a semantically-analyzed tuple construction expression.
|
||||
type TupleConsExpression struct {
|
||||
// The syntax node associated with the tuple construction expression.
|
||||
Syntax *hclsyntax.TupleConsExpr
|
||||
|
||||
// The elements of the tuple.
|
||||
Expressions []Expression
|
||||
|
||||
exprType Type
|
||||
}
|
||||
|
||||
// SyntaxNode returns the syntax node associated with the tuple construction expression.
|
||||
func (x *TupleConsExpression) SyntaxNode() hclsyntax.Node {
|
||||
return x.Syntax
|
||||
}
|
||||
|
||||
// Type returns the type of the tuple construction expression.
|
||||
func (x *TupleConsExpression) Type() Type {
|
||||
return x.exprType
|
||||
}
|
||||
|
||||
func (*TupleConsExpression) isExpression() {}
|
||||
|
||||
// UnaryOpExpression represents a semantically-analyzed unary operation.
|
||||
type UnaryOpExpression struct {
|
||||
// The syntax node associated with the unary operation.
|
||||
Syntax *hclsyntax.UnaryOpExpr
|
||||
|
||||
// The operand of the operation.
|
||||
Operand Expression
|
||||
|
||||
exprType Type
|
||||
}
|
||||
|
||||
// SyntaxNode returns the syntax node associated with the unary operation.
|
||||
func (x *UnaryOpExpression) SyntaxNode() hclsyntax.Node {
|
||||
return x.Syntax
|
||||
}
|
||||
|
||||
// Type returns the type of the unary operation.
|
||||
func (x *UnaryOpExpression) Type() Type {
|
||||
return x.exprType
|
||||
}
|
||||
|
||||
func (*UnaryOpExpression) isExpression() {}
|
26
pkg/codegen/hcl2/model/format/format.go
Normal file
26
pkg/codegen/hcl2/model/format/format.go
Normal file
|
@ -0,0 +1,26 @@
|
|||
// 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 format
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Func is a function type that implements the fmt.Formatter interface. This can be used to conveniently
|
||||
// implement this interface for types defined in other packages.
|
||||
type Func func(f fmt.State, c rune)
|
||||
|
||||
// Format invokes the Func's underlying function.
|
||||
func (p Func) Format(f fmt.State, c rune) {
|
||||
p(f, c)
|
||||
}
|
162
pkg/codegen/hcl2/model/format/gen.go
Normal file
162
pkg/codegen/hcl2/model/format/gen.go
Normal file
|
@ -0,0 +1,162 @@
|
|||
// 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 format
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/pulumi/pulumi/pkg/codegen/hcl2/model"
|
||||
"github.com/pulumi/pulumi/sdk/go/common/util/contract"
|
||||
)
|
||||
|
||||
// ExpressionGenerator is an interface that can be implemented in order to generate code for semantically-analyzed HCL2
|
||||
// expressions using a Formatter.
|
||||
type ExpressionGenerator interface {
|
||||
// GenAnonymousFunctionExpression generates code for an AnonymousFunctionExpression.
|
||||
GenAnonymousFunctionExpression(w io.Writer, expr *model.AnonymousFunctionExpression)
|
||||
// GenBinaryOpExpression generates code for a BinaryOpExpression.
|
||||
GenBinaryOpExpression(w io.Writer, expr *model.BinaryOpExpression)
|
||||
// GenConditionalExpression generates code for a ConditionalExpression.
|
||||
GenConditionalExpression(w io.Writer, expr *model.ConditionalExpression)
|
||||
// GenForExpression generates code for a ForExpression.
|
||||
GenForExpression(w io.Writer, expr *model.ForExpression)
|
||||
// GenFunctionCallExpression generates code for a FunctionCallExpression.
|
||||
GenFunctionCallExpression(w io.Writer, expr *model.FunctionCallExpression)
|
||||
// GenIndexExpression generates code for an IndexExpression.
|
||||
GenIndexExpression(w io.Writer, expr *model.IndexExpression)
|
||||
// GenLiteralValueExpression generates code for a LiteralValueExpression.
|
||||
GenLiteralValueExpression(w io.Writer, expr *model.LiteralValueExpression)
|
||||
// GenObjectConsExpression generates code for an ObjectConsExpression.
|
||||
GenObjectConsExpression(w io.Writer, expr *model.ObjectConsExpression)
|
||||
// GenRelativeTraversalExpression generates code for a RelativeTraversalExpression.
|
||||
GenRelativeTraversalExpression(w io.Writer, expr *model.RelativeTraversalExpression)
|
||||
// GenScopeTraversalExpression generates code for a ScopeTraversalExpression.
|
||||
GenScopeTraversalExpression(w io.Writer, expr *model.ScopeTraversalExpression)
|
||||
// GenSplatExpression generates code for a SplatExpression.
|
||||
GenSplatExpression(w io.Writer, expr *model.SplatExpression)
|
||||
// GenTemplateExpression generates code for a TemplateExpression.
|
||||
GenTemplateExpression(w io.Writer, expr *model.TemplateExpression)
|
||||
// GenTemplateJoinExpression generates code for a TemplateJoinExpression.
|
||||
GenTemplateJoinExpression(w io.Writer, expr *model.TemplateJoinExpression)
|
||||
// GenTupleConsExpression generates code for a TupleConsExpression.
|
||||
GenTupleConsExpression(w io.Writer, expr *model.TupleConsExpression)
|
||||
// GenUnaryOpExpression generates code for a UnaryOpExpression.
|
||||
GenUnaryOpExpression(w io.Writer, expr *model.UnaryOpExpression)
|
||||
}
|
||||
|
||||
// Formatter is a convenience type that implements a number of common utilities used to emit source code. It implements
|
||||
// the io.Writer interface.
|
||||
type Formatter struct {
|
||||
// The current indent level as a string.
|
||||
Indent string
|
||||
|
||||
// The ExpressionGenerator to use in {G,Fg}en{,f}
|
||||
g ExpressionGenerator
|
||||
}
|
||||
|
||||
// NewFormatter creates a new emitter targeting the given io.Writer that will use the given ExpressionGenerator when
|
||||
// generating code.
|
||||
func NewFormatter(g ExpressionGenerator) *Formatter {
|
||||
return &Formatter{g: g}
|
||||
}
|
||||
|
||||
// Indented bumps the current indentation level, invokes the given function, and then resets the indentation level to
|
||||
// its prior value.
|
||||
func (e *Formatter) Indented(f func()) {
|
||||
e.Indent += " "
|
||||
f()
|
||||
e.Indent = e.Indent[:len(e.Indent)-4]
|
||||
}
|
||||
|
||||
// Fprint prints one or more values to the generator's output stream.
|
||||
func (e *Formatter) Fprint(w io.Writer, a ...interface{}) {
|
||||
_, err := fmt.Fprint(w, a...)
|
||||
contract.IgnoreError(err)
|
||||
}
|
||||
|
||||
// Fprintln prints one or more values to the generator's output stream, followed by a newline.
|
||||
func (e *Formatter) Fprintln(w io.Writer, a ...interface{}) {
|
||||
e.Fprint(w, a...)
|
||||
e.Fprint(w, "\n")
|
||||
}
|
||||
|
||||
// Fprintf prints a formatted message to the generator's output stream.
|
||||
func (e *Formatter) Fprintf(w io.Writer, format string, a ...interface{}) {
|
||||
_, err := fmt.Fprintf(w, format, a...)
|
||||
contract.IgnoreError(err)
|
||||
}
|
||||
|
||||
// Fgen generates code for a list of strings and expression trees. The former are written directly to the destination;
|
||||
// the latter are recursively generated using the appropriate gen* functions.
|
||||
func (e *Formatter) Fgen(w io.Writer, vs ...interface{}) {
|
||||
for _, v := range vs {
|
||||
switch v := v.(type) {
|
||||
case string:
|
||||
_, err := fmt.Fprint(w, v)
|
||||
contract.IgnoreError(err)
|
||||
case *model.AnonymousFunctionExpression:
|
||||
e.g.GenAnonymousFunctionExpression(w, v)
|
||||
case *model.BinaryOpExpression:
|
||||
e.g.GenBinaryOpExpression(w, v)
|
||||
case *model.ConditionalExpression:
|
||||
e.g.GenConditionalExpression(w, v)
|
||||
case *model.ForExpression:
|
||||
e.g.GenForExpression(w, v)
|
||||
case *model.FunctionCallExpression:
|
||||
e.g.GenFunctionCallExpression(w, v)
|
||||
case *model.IndexExpression:
|
||||
e.g.GenIndexExpression(w, v)
|
||||
case *model.LiteralValueExpression:
|
||||
e.g.GenLiteralValueExpression(w, v)
|
||||
case *model.ObjectConsExpression:
|
||||
e.g.GenObjectConsExpression(w, v)
|
||||
case *model.RelativeTraversalExpression:
|
||||
e.g.GenRelativeTraversalExpression(w, v)
|
||||
case *model.ScopeTraversalExpression:
|
||||
e.g.GenScopeTraversalExpression(w, v)
|
||||
case *model.SplatExpression:
|
||||
e.g.GenSplatExpression(w, v)
|
||||
case *model.TemplateExpression:
|
||||
e.g.GenTemplateExpression(w, v)
|
||||
case *model.TemplateJoinExpression:
|
||||
e.g.GenTemplateJoinExpression(w, v)
|
||||
case *model.TupleConsExpression:
|
||||
e.g.GenTupleConsExpression(w, v)
|
||||
case *model.UnaryOpExpression:
|
||||
e.g.GenUnaryOpExpression(w, v)
|
||||
default:
|
||||
var rng hcl.Range
|
||||
if v, isExpr := v.(model.Expression); isExpr {
|
||||
rng = v.SyntaxNode().Range()
|
||||
}
|
||||
contract.Failf("unexpected expression node of type %T (%v)", v, rng)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fgenf generates code using a format string and its arguments. Any arguments that are BoundNode values are wrapped in
|
||||
// a Func that calls the appropriate recursive generation function. This allows for the composition of standard
|
||||
// format strings with expression/property code gen (e.e. `e.genf(w, ".apply(__arg0 => %v)", then)`, where `then` is
|
||||
// an expression tree).
|
||||
func (e *Formatter) Fgenf(w io.Writer, format string, args ...interface{}) {
|
||||
for i := range args {
|
||||
if node, ok := args[i].(model.Expression); ok {
|
||||
args[i] = Func(func(f fmt.State, c rune) { e.Fgen(f, node) })
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(w, format, args...)
|
||||
}
|
83
pkg/codegen/hcl2/model/functions.go
Normal file
83
pkg/codegen/hcl2/model/functions.go
Normal file
|
@ -0,0 +1,83 @@
|
|||
// 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 model
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||
"github.com/pulumi/pulumi/pkg/codegen/hcl2/syntax"
|
||||
)
|
||||
|
||||
// FunctionSignature represents a possibly-type-polymorphic function signature.
|
||||
type FunctionSignature interface {
|
||||
// GetSignature returns the static signature for the function when invoked with the given arguments.
|
||||
GetSignature(arguments []Expression) (StaticFunctionSignature, hcl.Diagnostics)
|
||||
}
|
||||
|
||||
// Parameter represents a single function parameter.
|
||||
type Parameter struct {
|
||||
Name string // The name of the parameter.
|
||||
Type Type // The type of the parameter.
|
||||
}
|
||||
|
||||
// StaticFunctionSignature records the parameters and return type of a function.
|
||||
type StaticFunctionSignature struct {
|
||||
// The function's fixed parameters.
|
||||
Parameters []Parameter
|
||||
// The function's variadic parameter, if any. Any arguments that follow a function's fixed arguments must be
|
||||
// assignable to this parameter.
|
||||
VarargsParameter *Parameter
|
||||
// The return type of the function.
|
||||
ReturnType Type
|
||||
}
|
||||
|
||||
// GetSignature returns the static signature itself.
|
||||
func (fs StaticFunctionSignature) GetSignature(arguments []Expression) (StaticFunctionSignature, hcl.Diagnostics) {
|
||||
return fs, nil
|
||||
}
|
||||
|
||||
// GenericFunctionSignature represents a type-polymorphic function signature. The underlying function will be
|
||||
// invoked by GenericFunctionSignature.GetSignature to compute the static signature of the function.
|
||||
type GenericFunctionSignature func(arguments []Expression) (StaticFunctionSignature, hcl.Diagnostics)
|
||||
|
||||
// GetSignature returns the static function signature when it is invoked with the given arguments.
|
||||
func (fs GenericFunctionSignature) GetSignature(arguments []Expression) (StaticFunctionSignature, hcl.Diagnostics) {
|
||||
return fs(arguments)
|
||||
}
|
||||
|
||||
// Function represents a function definition.
|
||||
type Function struct {
|
||||
signature FunctionSignature
|
||||
}
|
||||
|
||||
// NewFunction creates a new function with the given signature.
|
||||
func NewFunction(signature FunctionSignature) *Function {
|
||||
return &Function{signature: signature}
|
||||
}
|
||||
|
||||
// SyntaxNode returns the syntax node for the function, which is always syntax.None.
|
||||
func (f *Function) SyntaxNode() hclsyntax.Node {
|
||||
return syntax.None
|
||||
}
|
||||
|
||||
// Traverse attempts to traverse the function definition. This will always fail: functions are not traversable.
|
||||
func (f *Function) Traverse(traverser hcl.Traverser) (Traversable, hcl.Diagnostics) {
|
||||
return DynamicType, hcl.Diagnostics{cannotTraverseFunction(traverser.SourceRange())}
|
||||
}
|
||||
|
||||
// GetSignature returns the static signature of the function when it is invoked with the given arguments.
|
||||
func (f *Function) GetSignature(arguments []Expression) (StaticFunctionSignature, hcl.Diagnostics) {
|
||||
return f.signature.GetSignature(arguments)
|
||||
}
|
209
pkg/codegen/hcl2/model/scope.go
Normal file
209
pkg/codegen/hcl2/model/scope.go
Normal file
|
@ -0,0 +1,209 @@
|
|||
// 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 model
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||
"github.com/pulumi/pulumi/pkg/codegen/hcl2/syntax"
|
||||
)
|
||||
|
||||
// Definition represents a single definition in a Scope.
|
||||
type Definition interface {
|
||||
Traversable
|
||||
|
||||
SyntaxNode() hclsyntax.Node
|
||||
}
|
||||
|
||||
// A Keyword is a non-traversable definition that allows scope traversals to bind to arbitrary keywords.
|
||||
type Keyword string
|
||||
|
||||
// Traverse attempts to traverse the keyword, and always fails.
|
||||
func (kw Keyword) Traverse(traverser hcl.Traverser) (Traversable, hcl.Diagnostics) {
|
||||
return DynamicType, hcl.Diagnostics{cannotTraverseKeyword(string(kw), traverser.SourceRange())}
|
||||
}
|
||||
|
||||
// SyntaxNode returns the syntax node for the keyword, which is always syntax.None.
|
||||
func (kw Keyword) SyntaxNode() hclsyntax.Node {
|
||||
return syntax.None
|
||||
}
|
||||
|
||||
// A Variable is a traversable, typed definition that represents a named value.
|
||||
type Variable struct {
|
||||
// The syntax node associated with the variable definition, if any.
|
||||
Syntax hclsyntax.Node
|
||||
|
||||
// The name of the variable.
|
||||
Name string
|
||||
// The type of the variable.
|
||||
VariableType Type
|
||||
}
|
||||
|
||||
// Traverse attempts to traverse the variable's type.
|
||||
func (v *Variable) Traverse(traverser hcl.Traverser) (Traversable, hcl.Diagnostics) {
|
||||
return v.VariableType.Traverse(traverser)
|
||||
}
|
||||
|
||||
// SyntaxNode returns the variable's syntax node or syntax.None.
|
||||
func (v *Variable) SyntaxNode() hclsyntax.Node {
|
||||
return syntaxOrNone(v.Syntax)
|
||||
}
|
||||
|
||||
// Type returns the type of the variable.
|
||||
func (v *Variable) Type() Type {
|
||||
return v.VariableType
|
||||
}
|
||||
|
||||
// A Scope is used to map names to definitions during expression binding.
|
||||
//
|
||||
// A scope has two namespaces: one that is exclusive to functions and one that contains both variables and functions.
|
||||
// When binding a reference, only the latter is checked; when binding a function, only the former is checked.
|
||||
type Scope struct {
|
||||
parent *Scope
|
||||
syntax hclsyntax.Node
|
||||
defs map[string]Definition
|
||||
functions map[string]*Function
|
||||
}
|
||||
|
||||
// NewRootScope returns a new unparented scope associated with the given syntax node.
|
||||
func NewRootScope(syntax hclsyntax.Node) *Scope {
|
||||
return &Scope{syntax: syntax, defs: map[string]Definition{}, functions: map[string]*Function{}}
|
||||
}
|
||||
|
||||
// Traverse attempts to traverse the scope using the given traverser. If the traverser is a literal string that refers
|
||||
// to a name defined within the scope or one of its ancestors, the traversal returns the corresponding definition.
|
||||
func (s *Scope) Traverse(traverser hcl.Traverser) (Traversable, hcl.Diagnostics) {
|
||||
name, nameType := GetTraverserKey(traverser)
|
||||
if nameType != StringType {
|
||||
return DynamicType, hcl.Diagnostics{undefinedVariable(traverser.SourceRange())}
|
||||
}
|
||||
|
||||
memberName := name.AsString()
|
||||
member, hasMember := s.BindReference(memberName)
|
||||
if !hasMember {
|
||||
return DynamicType, hcl.Diagnostics{undefinedVariable(traverser.SourceRange())}
|
||||
}
|
||||
return member, nil
|
||||
}
|
||||
|
||||
// SyntaxNode returns the syntax node associated with the scope, if any.
|
||||
func (s *Scope) SyntaxNode() hclsyntax.Node {
|
||||
if s != nil {
|
||||
return syntaxOrNone(s.syntax)
|
||||
}
|
||||
return syntax.None
|
||||
}
|
||||
|
||||
// BindReference returns the definition that corresponds to the given name, if any. Each parent scope is checked until
|
||||
// a definition is found or no parent scope remains.
|
||||
func (s *Scope) BindReference(name string) (Definition, bool) {
|
||||
if s != nil {
|
||||
if def, ok := s.defs[name]; ok {
|
||||
return def, true
|
||||
}
|
||||
if s.parent != nil {
|
||||
return s.parent.BindReference(name)
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// BindFunctionReference returns the function definition that corresponds to the given name, if any. Each parent scope
|
||||
// is checked until a definition is found or no parent scope remains.
|
||||
func (s *Scope) BindFunctionReference(name string) (*Function, bool) {
|
||||
if s != nil {
|
||||
if fn, ok := s.functions[name]; ok {
|
||||
return fn, true
|
||||
}
|
||||
if s.parent != nil {
|
||||
return s.parent.BindFunctionReference(name)
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Define maps the given name to the given definition. If the name is already defined in this scope, the existing
|
||||
// definition is not overwritten and Define returns false.
|
||||
func (s *Scope) Define(name string, def Definition) bool {
|
||||
if s != nil {
|
||||
if _, hasDef := s.defs[name]; !hasDef {
|
||||
s.defs[name] = def
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// DefineFunction maps the given function name to the given function definition. If the function is alreadu defined in
|
||||
// this scope, the definition is not overwritten and DefineFunction returns false.
|
||||
func (s *Scope) DefineFunction(name string, def *Function) bool {
|
||||
if s != nil {
|
||||
if _, hasFunc := s.functions[name]; !hasFunc {
|
||||
s.functions[name] = def
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// DefineScope defines a child scope with the given name. If the name is already defined in this scope, the existing
|
||||
// definition is not overwritten and DefineScope returns false.
|
||||
func (s *Scope) DefineScope(name string, syntax hclsyntax.Node) (*Scope, bool) {
|
||||
if s != nil {
|
||||
if _, exists := s.defs[name]; !exists {
|
||||
child := &Scope{parent: s, syntax: syntax, defs: map[string]Definition{}, functions: map[string]*Function{}}
|
||||
s.defs[name] = child
|
||||
return child, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Push defines an anonymous child scope associated with the given syntax node.
|
||||
func (s *Scope) Push(syntax hclsyntax.Node) *Scope {
|
||||
return &Scope{parent: s, syntax: syntax, defs: map[string]Definition{}, functions: map[string]*Function{}}
|
||||
}
|
||||
|
||||
// Pop returns this scope's parent.
|
||||
func (s *Scope) Pop() *Scope {
|
||||
return s.parent
|
||||
}
|
||||
|
||||
// Scopes is the interface that is used fetch the scope that should be used when binding a block or attribute.
|
||||
type Scopes interface {
|
||||
// GetScopesForBlock returns the Scopes that should be used when binding the given block.
|
||||
GetScopesForBlock(block *hclsyntax.Block) (Scopes, hcl.Diagnostics)
|
||||
// GetScopeForAttribute returns the *Scope that should be used when binding the given attribute.
|
||||
GetScopeForAttribute(attribute *hclsyntax.Attribute) (*Scope, hcl.Diagnostics)
|
||||
}
|
||||
|
||||
type staticScope struct {
|
||||
scope *Scope
|
||||
}
|
||||
|
||||
// GetScopesForBlock returns the scopes to use when binding the given block.
|
||||
func (s staticScope) GetScopesForBlock(block *hclsyntax.Block) (Scopes, hcl.Diagnostics) {
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// GetScopeForAttribute returns the scope to use when binding the given attribute.
|
||||
func (s staticScope) GetScopeForAttribute(attribute *hclsyntax.Attribute) (*Scope, hcl.Diagnostics) {
|
||||
return s.scope, nil
|
||||
}
|
||||
|
||||
// StaticScope returns a Scopes that uses the given *Scope for all blocks and attributes.
|
||||
func StaticScope(scope *Scope) Scopes {
|
||||
return staticScope{scope: scope}
|
||||
}
|
114
pkg/codegen/hcl2/model/spec.md
Normal file
114
pkg/codegen/hcl2/model/spec.md
Normal file
|
@ -0,0 +1,114 @@
|
|||
# HCL Syntax-Agnostic Information Model Extensions
|
||||
|
||||
This document describes extensions to the HCL Syntax-Agnostic Information
|
||||
Model that are implemented by this package. The original specification can be
|
||||
found [here](https://github.com/hashicorp/hcl/blob/v2.3.0/spec.md).
|
||||
|
||||
## Extended Types
|
||||
|
||||
### Primitive Types
|
||||
|
||||
The extended type system two additional primitive types, _int_.
|
||||
|
||||
An _int_ is an arbitrary-precision integer value. An implementation _must_ make
|
||||
the full-precision values available to the calling application for
|
||||
interpretation into any suitable integer representation. An implementation may
|
||||
in practice implement ints with limited precision so long as the following
|
||||
constraints are met:
|
||||
|
||||
- Integers are represented with at least 256 bits.
|
||||
- An error is produced if an integer value given in source cannot be
|
||||
represented precisely.
|
||||
|
||||
Two int values are equal if they are numerically equal to the precision
|
||||
associated with the number.
|
||||
|
||||
Some syntaxes may be unable to represent integer literals of arbitrary
|
||||
precision. This must be defined in the syntax specification as part of its
|
||||
description of mapping numeric literals to HCL values.
|
||||
|
||||
### Structural Types
|
||||
|
||||
The extended type system adds a new structural type kind, _union_.
|
||||
|
||||
A _union type_ is constructed of a set of types. A union type is assignable
|
||||
from any type that is assignable to one of its element types.
|
||||
|
||||
A union type is traversed by traversing each of its element types. The result
|
||||
of the traversal is the union of the results of the traversals that succeed.
|
||||
When traversing a union with an element type of none, the traversal of none
|
||||
successfully results in none; this allows a traversal of an optional value to
|
||||
return an optional value of the appropriate type.
|
||||
|
||||
### Eventual Types
|
||||
|
||||
The extended type system adds two _eventual type kinds_, _promise_ and
|
||||
_output_. These types represent values that are only available asynchronously,
|
||||
and can be used by applications that produce such values to more accurately
|
||||
track which values are available promptly and which are not.
|
||||
|
||||
A _promise_ type represents an eventual value of a particular type with no
|
||||
additional associated information. A promise type is assignable from itself
|
||||
or from its element type. Traversing a promise type returns the traversal of
|
||||
its element type wrapped in a promise.
|
||||
|
||||
An _output_ type represents an eventual value of a particular type that carries
|
||||
additional application-specific information. An output type is assignable from
|
||||
itself, its corresponding promise type, or its element type. Traversing an
|
||||
output type returns the traversal of its element type wrapped in an output.
|
||||
|
||||
### Null values
|
||||
|
||||
The extended type system includes a first-class representation for the null
|
||||
value, the _none_ type. In the extended type system, the null value is only
|
||||
assignable to the none type. Optional values of type T are represented by
|
||||
the type `union(T, none)`.
|
||||
|
||||
## Type Conversions and Unification
|
||||
|
||||
### Primitive Type Conversions
|
||||
|
||||
Bidirectional conversions are available between the string and int types and
|
||||
the number and int types. Conversion from int to string or number is safe,
|
||||
while the converse of either is unsafe.
|
||||
|
||||
### Collection and Structural Type Conversions
|
||||
|
||||
Conversion from a type T to a union type is permitted if there is a conversion
|
||||
from T to at least one of the union's element types. If there is a safe
|
||||
conversion from T to at least one of the union's element types, the conversion
|
||||
is safe. Otherwise, the conversion is unsafe.
|
||||
|
||||
### Eventual Type Conversions
|
||||
|
||||
Conversion from a type T to a promise with element type U is permitted if T is
|
||||
a promise with element type V where V is convertible to U or if T is
|
||||
convertible to U. The safety of this conversion depends on the safety of the
|
||||
conversion from V or T to U.
|
||||
|
||||
Conversion from a type T to an output with element type U is permitted if T is
|
||||
an output or promise with element type V where V is convertible to U or if T is
|
||||
convertible to U. The safety of this conversion depends on the safety of the
|
||||
conversion from V or T to U.
|
||||
|
||||
### Type Unification
|
||||
|
||||
The int type unifies with number by preferring number, and unifies with string
|
||||
by preferring string.
|
||||
|
||||
Two union types unify by producing a new union type whose elements are the
|
||||
concatenation of those of the two input types.
|
||||
|
||||
A union type unifies with another type by producing a new union whose element
|
||||
types are the unification of the other type with each of the input union's
|
||||
element types.
|
||||
|
||||
A promise type unifies with an output type by producing a new output type whose
|
||||
element type is the unification of the output type's element type and the promise
|
||||
type's element types.
|
||||
|
||||
Two promise types unify by producing a new promise type whose element type is the
|
||||
unification of the element types of the two promise types.
|
||||
|
||||
Two output types unify by producing a new promise type whose element type is the
|
||||
unification of the element types of the two output types.
|
89
pkg/codegen/hcl2/model/traversable.go
Normal file
89
pkg/codegen/hcl2/model/traversable.go
Normal file
|
@ -0,0 +1,89 @@
|
|||
// 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 model
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/pulumi/pulumi/sdk/go/common/util/contract"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
// Traversable represents an entity that can be traversed by an HCL2 traverser.
|
||||
type Traversable interface {
|
||||
// Traverse attempts to traverse the receiver using the given traverser.
|
||||
Traverse(t hcl.Traverser) (Traversable, hcl.Diagnostics)
|
||||
}
|
||||
|
||||
// TypedTraversable is a Traversable that has an associated type.
|
||||
type TypedTraversable interface {
|
||||
Traversable
|
||||
|
||||
Type() Type
|
||||
}
|
||||
|
||||
// GetTraversableType returns the type of the given Traversable:
|
||||
// - If the Traversable is a TypedTraversable, this returns t.Type()
|
||||
// - If the Traversable is a Type, this returns t
|
||||
// - Otherwise, this returns DynamicType
|
||||
func GetTraversableType(t Traversable) Type {
|
||||
switch t := t.(type) {
|
||||
case TypedTraversable:
|
||||
return t.Type()
|
||||
case Type:
|
||||
return t
|
||||
default:
|
||||
return DynamicType
|
||||
}
|
||||
}
|
||||
|
||||
// GetTraverserKey extracts the value and type of the key associated with the given traverser.
|
||||
func GetTraverserKey(t hcl.Traverser) (cty.Value, Type) {
|
||||
switch t := t.(type) {
|
||||
case hcl.TraverseAttr:
|
||||
return cty.StringVal(t.Name), StringType
|
||||
case hcl.TraverseIndex:
|
||||
if t.Key.Type().Equals(typeCapsule) {
|
||||
return cty.DynamicVal, *(t.Key.EncapsulatedValue().(*Type))
|
||||
}
|
||||
return t.Key, ctyTypeToType(t.Key.Type(), false)
|
||||
default:
|
||||
contract.Failf("unexpected traverser of type %T (%v)", t, t.SourceRange())
|
||||
return cty.DynamicVal, DynamicType
|
||||
}
|
||||
}
|
||||
|
||||
// bindTraversalParts computes the type for each element of the given traversal.
|
||||
func (b *expressionBinder) bindTraversalParts(receiver Traversable,
|
||||
traversal hcl.Traversal) ([]Traversable, hcl.Diagnostics) {
|
||||
|
||||
parts := make([]Traversable, len(traversal)+1)
|
||||
parts[0] = receiver
|
||||
|
||||
var diagnostics hcl.Diagnostics
|
||||
for i, part := range traversal {
|
||||
nextReceiver, partDiags := parts[i].Traverse(part)
|
||||
parts[i+1], diagnostics = nextReceiver, append(diagnostics, partDiags...)
|
||||
}
|
||||
|
||||
switch parts[len(parts)-1].(type) {
|
||||
case TypedTraversable, Type:
|
||||
// OK
|
||||
default:
|
||||
// TODO(pdg): improve this diagnostic?
|
||||
diagnostics = append(diagnostics, undefinedVariable(traversal.SourceRange()))
|
||||
}
|
||||
|
||||
return parts, diagnostics
|
||||
}
|
158
pkg/codegen/hcl2/model/type.go
Normal file
158
pkg/codegen/hcl2/model/type.go
Normal file
|
@ -0,0 +1,158 @@
|
|||
// 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 model
|
||||
|
||||
import (
|
||||
"github.com/pulumi/pulumi/sdk/go/common/util/contract"
|
||||
)
|
||||
|
||||
type ConversionKind int
|
||||
|
||||
const (
|
||||
NoConversion ConversionKind = 0
|
||||
UnsafeConversion ConversionKind = 1
|
||||
SafeConversion ConversionKind = 2
|
||||
)
|
||||
|
||||
func (k ConversionKind) Exists() bool {
|
||||
return k > NoConversion && k <= SafeConversion
|
||||
}
|
||||
|
||||
// Type represents a datatype in the Pulumi Schema. Types created by this package are identical if they are
|
||||
// equal values.
|
||||
type Type interface {
|
||||
Definition
|
||||
|
||||
AssignableFrom(src Type) bool
|
||||
ConversionFrom(src Type) ConversionKind
|
||||
String() string
|
||||
|
||||
conversionFrom(src Type, unifying bool) ConversionKind
|
||||
unify(other Type) (Type, ConversionKind)
|
||||
isType()
|
||||
}
|
||||
|
||||
var (
|
||||
// NoneType represents the undefined value.
|
||||
NoneType Type = noneType(0)
|
||||
// BoolType represents the set of boolean values.
|
||||
BoolType = MustNewOpaqueType("boolean")
|
||||
// IntType represents the set of 32-bit integer values.
|
||||
IntType = MustNewOpaqueType("int")
|
||||
// NumberType represents the set of arbitrary-precision values.
|
||||
NumberType = MustNewOpaqueType("number")
|
||||
// StringType represents the set of UTF-8 string values.
|
||||
StringType = MustNewOpaqueType("string")
|
||||
// DynamicType represents the set of all values.
|
||||
DynamicType = MustNewOpaqueType("dynamic")
|
||||
)
|
||||
|
||||
func assignableFrom(dest, src Type, assignableFrom func() bool) bool {
|
||||
if dest == src || dest == DynamicType {
|
||||
return true
|
||||
}
|
||||
if src, isUnion := src.(*UnionType); isUnion {
|
||||
return src.assignableTo(dest)
|
||||
}
|
||||
if src == DynamicType {
|
||||
return false
|
||||
}
|
||||
return assignableFrom()
|
||||
}
|
||||
|
||||
func conversionFrom(dest, src Type, unifying bool, conversionFrom func() ConversionKind) ConversionKind {
|
||||
if dest == src || dest == DynamicType {
|
||||
return SafeConversion
|
||||
}
|
||||
if src, isUnion := src.(*UnionType); isUnion {
|
||||
return src.conversionTo(dest, unifying)
|
||||
}
|
||||
if src == DynamicType {
|
||||
return UnsafeConversion
|
||||
}
|
||||
return conversionFrom()
|
||||
}
|
||||
|
||||
func unify(t0, t1 Type, unify func() (Type, ConversionKind)) (Type, ConversionKind) {
|
||||
contract.Assert(t0 != nil)
|
||||
|
||||
// Normalize s.t. dynamic is always on the right.
|
||||
if t0 == DynamicType {
|
||||
t0, t1 = t1, t0
|
||||
}
|
||||
|
||||
switch {
|
||||
case t0 == t1:
|
||||
return t0, SafeConversion
|
||||
case t1 == DynamicType:
|
||||
// The dynamic type unifies with any other type by selecting that other type.
|
||||
return t0, UnsafeConversion
|
||||
default:
|
||||
conversionFrom, conversionTo := t0.conversionFrom(t1, true), t1.conversionFrom(t0, true)
|
||||
switch {
|
||||
case conversionFrom < conversionTo:
|
||||
return t1, conversionTo
|
||||
case conversionFrom > conversionTo:
|
||||
return t0, conversionFrom
|
||||
}
|
||||
if conversionFrom == NoConversion {
|
||||
return NewUnionType(t0, t1), SafeConversion
|
||||
}
|
||||
if union, ok := t1.(*UnionType); ok {
|
||||
return union.unifyTo(t0)
|
||||
}
|
||||
|
||||
unified, conversionKind := unify()
|
||||
contract.Assert(conversionKind >= conversionFrom)
|
||||
contract.Assert(conversionKind >= conversionTo)
|
||||
return unified, conversionKind
|
||||
}
|
||||
}
|
||||
|
||||
// UnifyTypes chooses the most general type that is convertible from all of the input types.
|
||||
func UnifyTypes(types ...Type) (safeType Type, unsafeType Type) {
|
||||
for _, t := range types {
|
||||
if safeType == nil {
|
||||
safeType = t
|
||||
} else {
|
||||
if safeT, safeConversion := safeType.unify(t); safeConversion >= SafeConversion {
|
||||
safeType = safeT
|
||||
} else {
|
||||
safeType = NewUnionType(safeType, t)
|
||||
}
|
||||
}
|
||||
|
||||
if unsafeType == nil {
|
||||
unsafeType = t
|
||||
} else {
|
||||
if unsafeT, unsafeConversion := unsafeType.unify(t); unsafeConversion >= UnsafeConversion {
|
||||
unsafeType = unsafeT
|
||||
} else {
|
||||
unsafeType = NewUnionType(unsafeType, t)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if safeType == nil {
|
||||
safeType = NoneType
|
||||
}
|
||||
if unsafeType == nil {
|
||||
unsafeType = NoneType
|
||||
}
|
||||
|
||||
contract.Assertf(unsafeType == safeType || unsafeType.ConversionFrom(safeType).Exists(),
|
||||
"no conversion from %v to %v", safeType, unsafeType)
|
||||
return safeType, unsafeType
|
||||
}
|
159
pkg/codegen/hcl2/model/type_eventuals.go
Normal file
159
pkg/codegen/hcl2/model/type_eventuals.go
Normal file
|
@ -0,0 +1,159 @@
|
|||
// 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 model
|
||||
|
||||
type typeTransform int
|
||||
|
||||
var (
|
||||
makeIdentity = typeTransform(0)
|
||||
makePromise = typeTransform(1)
|
||||
makeOutput = typeTransform(2)
|
||||
)
|
||||
|
||||
func (f typeTransform) do(t Type) Type {
|
||||
switch f {
|
||||
case makePromise:
|
||||
return NewPromiseType(t)
|
||||
case makeOutput:
|
||||
return NewOutputType(t)
|
||||
default:
|
||||
return t
|
||||
}
|
||||
}
|
||||
|
||||
func resolveEventuals(t Type, resolveOutputs bool) (Type, typeTransform) {
|
||||
switch t := t.(type) {
|
||||
case *OutputType:
|
||||
if resolveOutputs {
|
||||
return t.ElementType, makeOutput
|
||||
}
|
||||
return t, makeIdentity
|
||||
case *PromiseType:
|
||||
element, transform := resolveEventuals(t.ElementType, resolveOutputs)
|
||||
if makePromise > transform {
|
||||
transform = makePromise
|
||||
}
|
||||
return element, transform
|
||||
case *MapType:
|
||||
resolved, transform := resolveEventuals(t.ElementType, resolveOutputs)
|
||||
return NewMapType(resolved), transform
|
||||
case *ListType:
|
||||
resolved, transform := resolveEventuals(t.ElementType, resolveOutputs)
|
||||
return NewListType(resolved), transform
|
||||
case *SetType:
|
||||
resolved, transform := resolveEventuals(t.ElementType, resolveOutputs)
|
||||
return NewSetType(resolved), transform
|
||||
case *UnionType:
|
||||
transform := makeIdentity
|
||||
elementTypes := make([]Type, len(t.ElementTypes))
|
||||
for i, t := range t.ElementTypes {
|
||||
element, elementTransform := resolveEventuals(t, resolveOutputs)
|
||||
if elementTransform > transform {
|
||||
transform = elementTransform
|
||||
}
|
||||
elementTypes[i] = element
|
||||
}
|
||||
return NewUnionType(elementTypes...), transform
|
||||
case *ObjectType:
|
||||
transform := makeIdentity
|
||||
properties := map[string]Type{}
|
||||
for k, t := range t.Properties {
|
||||
property, propertyTransform := resolveEventuals(t, resolveOutputs)
|
||||
if propertyTransform > transform {
|
||||
transform = propertyTransform
|
||||
}
|
||||
properties[k] = property
|
||||
}
|
||||
return NewObjectType(properties), transform
|
||||
case *TupleType:
|
||||
transform := makeIdentity
|
||||
elements := make([]Type, len(t.ElementTypes))
|
||||
for i, t := range t.ElementTypes {
|
||||
element, elementTransform := resolveEventuals(t, resolveOutputs)
|
||||
if elementTransform > transform {
|
||||
transform = elementTransform
|
||||
}
|
||||
elements[i] = element
|
||||
}
|
||||
return NewTupleType(elements...), transform
|
||||
default:
|
||||
return t, makeIdentity
|
||||
}
|
||||
}
|
||||
|
||||
// ResolveOutputs recursively replaces all output(T) and promise(T) types in the input type with their element type.
|
||||
func ResolveOutputs(t Type) Type {
|
||||
resolved, _ := resolveEventuals(t, true)
|
||||
return resolved
|
||||
}
|
||||
|
||||
// ResolvePromises recursively replaces all promise(T) types in the input type with their element type.
|
||||
func ResolvePromises(t Type) Type {
|
||||
resolved, _ := resolveEventuals(t, false)
|
||||
return resolved
|
||||
}
|
||||
|
||||
func liftOperationType(resultType Type, arguments ...Expression) Type {
|
||||
var transform typeTransform
|
||||
for _, arg := range arguments {
|
||||
_, t := resolveEventuals(arg.Type(), true)
|
||||
if t > transform {
|
||||
transform = t
|
||||
}
|
||||
}
|
||||
return transform.do(resultType)
|
||||
}
|
||||
|
||||
var inputTypes = map[Type]Type{}
|
||||
|
||||
// InputType returns the result of replacing each type in T with union(T, output(T)).
|
||||
func InputType(t Type) Type {
|
||||
if t == DynamicType || t == NoneType {
|
||||
return t
|
||||
}
|
||||
if input, ok := inputTypes[t]; ok {
|
||||
return input
|
||||
}
|
||||
|
||||
var src Type
|
||||
switch t := t.(type) {
|
||||
case *OutputType:
|
||||
return t
|
||||
case *PromiseType:
|
||||
src = NewPromiseType(InputType(t.ElementType))
|
||||
case *MapType:
|
||||
src = NewMapType(InputType(t.ElementType))
|
||||
case *ListType:
|
||||
src = NewListType(InputType(t.ElementType))
|
||||
case *UnionType:
|
||||
elementTypes := make([]Type, len(t.ElementTypes))
|
||||
for i, t := range t.ElementTypes {
|
||||
elementTypes[i] = InputType(t)
|
||||
}
|
||||
src = NewUnionType(elementTypes...)
|
||||
case *ObjectType:
|
||||
properties := map[string]Type{}
|
||||
for k, t := range t.Properties {
|
||||
properties[k] = InputType(t)
|
||||
}
|
||||
src = NewObjectType(properties)
|
||||
default:
|
||||
src = t
|
||||
}
|
||||
|
||||
input := NewUnionType(src, NewOutputType(src))
|
||||
inputTypes[t] = input
|
||||
return input
|
||||
}
|
143
pkg/codegen/hcl2/model/type_list.go
Normal file
143
pkg/codegen/hcl2/model/type_list.go
Normal file
|
@ -0,0 +1,143 @@
|
|||
// 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 model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||
"github.com/pulumi/pulumi/pkg/codegen/hcl2/syntax"
|
||||
)
|
||||
|
||||
// ListType represents lists of particular element types.
|
||||
type ListType struct {
|
||||
// ElementType is the element type of the list.
|
||||
ElementType Type
|
||||
|
||||
s string
|
||||
}
|
||||
|
||||
// The set of list types, indexed by element type.
|
||||
var listTypes = map[Type]*ListType{}
|
||||
|
||||
// NewListType creates a new list type with the given element type.
|
||||
func NewListType(elementType Type) *ListType {
|
||||
if t, ok := listTypes[elementType]; ok {
|
||||
return t
|
||||
}
|
||||
|
||||
t := &ListType{ElementType: elementType}
|
||||
listTypes[elementType] = t
|
||||
return t
|
||||
}
|
||||
|
||||
// SyntaxNode returns the syntax node for the type. This is always syntax.None.
|
||||
func (*ListType) SyntaxNode() hclsyntax.Node {
|
||||
return syntax.None
|
||||
}
|
||||
|
||||
// Traverse attempts to traverse the optional type with the given traverser. The result type of traverse(list(T))
|
||||
// is T; the traversal fails if the traverser is not a number.
|
||||
func (t *ListType) Traverse(traverser hcl.Traverser) (Traversable, hcl.Diagnostics) {
|
||||
_, indexType := GetTraverserKey(traverser)
|
||||
|
||||
var diagnostics hcl.Diagnostics
|
||||
if !InputType(NumberType).ConversionFrom(indexType).Exists() {
|
||||
diagnostics = hcl.Diagnostics{unsupportedListIndex(traverser.SourceRange())}
|
||||
}
|
||||
return t.ElementType, diagnostics
|
||||
}
|
||||
|
||||
// AssignableFrom returns true if this type is assignable from the indicated source type. A list(T) is assignable
|
||||
// from values of type list(U) where T is assignable from U.
|
||||
func (t *ListType) AssignableFrom(src Type) bool {
|
||||
return assignableFrom(t, src, func() bool {
|
||||
switch src := src.(type) {
|
||||
case *ListType:
|
||||
return t.ElementType.AssignableFrom(src.ElementType)
|
||||
case *TupleType:
|
||||
for _, src := range src.ElementTypes {
|
||||
if !t.ElementType.AssignableFrom(src) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
func (t *ListType) ConversionFrom(src Type) ConversionKind {
|
||||
return t.conversionFrom(src, false)
|
||||
}
|
||||
|
||||
func (t *ListType) conversionFrom(src Type, unifying bool) ConversionKind {
|
||||
return conversionFrom(t, src, unifying, func() ConversionKind {
|
||||
switch src := src.(type) {
|
||||
case *ListType:
|
||||
return t.ElementType.conversionFrom(src.ElementType, unifying)
|
||||
case *SetType:
|
||||
return t.ElementType.conversionFrom(src.ElementType, unifying)
|
||||
case *TupleType:
|
||||
conversionKind := SafeConversion
|
||||
for _, src := range src.ElementTypes {
|
||||
if ck := t.ElementType.conversionFrom(src, unifying); ck < conversionKind {
|
||||
conversionKind = ck
|
||||
}
|
||||
}
|
||||
return conversionKind
|
||||
}
|
||||
return NoConversion
|
||||
})
|
||||
}
|
||||
|
||||
func (t *ListType) String() string {
|
||||
if t.s == "" {
|
||||
t.s = fmt.Sprintf("list(%v)", t.ElementType)
|
||||
}
|
||||
return t.s
|
||||
}
|
||||
|
||||
func (t *ListType) unify(other Type) (Type, ConversionKind) {
|
||||
return unify(t, other, func() (Type, ConversionKind) {
|
||||
switch other := other.(type) {
|
||||
case *TupleType:
|
||||
// If the other element is a list type, prefer the list type, but unify the element type.
|
||||
elementType, conversionKind := t.ElementType, SafeConversion
|
||||
for _, other := range other.ElementTypes {
|
||||
element, ck := elementType.unify(other)
|
||||
if ck < conversionKind {
|
||||
conversionKind = ck
|
||||
}
|
||||
elementType = element
|
||||
}
|
||||
return NewListType(elementType), conversionKind
|
||||
case *SetType:
|
||||
// If the other element is a set type, prefer the list type, but unify the element types.
|
||||
elementType, conversionKind := t.ElementType.unify(other.ElementType)
|
||||
return NewListType(elementType), conversionKind
|
||||
case *ListType:
|
||||
// If the other type is a list type, unify based on the element type.
|
||||
elementType, conversionKind := t.ElementType.unify(other.ElementType)
|
||||
return NewListType(elementType), conversionKind
|
||||
default:
|
||||
// Prefer the list type.
|
||||
return t, t.conversionFrom(other, true)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (*ListType) isType() {}
|
138
pkg/codegen/hcl2/model/type_map.go
Normal file
138
pkg/codegen/hcl2/model/type_map.go
Normal file
|
@ -0,0 +1,138 @@
|
|||
// 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 model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||
"github.com/pulumi/pulumi/pkg/codegen/hcl2/syntax"
|
||||
)
|
||||
|
||||
// MapType represents maps from strings to particular element types.
|
||||
type MapType struct {
|
||||
// ElementType is the element type of the map.
|
||||
ElementType Type
|
||||
|
||||
s string
|
||||
}
|
||||
|
||||
// The set of map types, indexed by element type.
|
||||
var mapTypes = map[Type]*MapType{}
|
||||
|
||||
// NewMapType creates a new map type with the given element type.
|
||||
func NewMapType(elementType Type) *MapType {
|
||||
if t, ok := mapTypes[elementType]; ok {
|
||||
return t
|
||||
}
|
||||
|
||||
t := &MapType{ElementType: elementType}
|
||||
mapTypes[elementType] = t
|
||||
return t
|
||||
}
|
||||
|
||||
// Traverse attempts to traverse the optional type with the given traverser. The result type of traverse(map(T))
|
||||
// is T; the traversal fails if the traverser is not a string.
|
||||
func (t *MapType) Traverse(traverser hcl.Traverser) (Traversable, hcl.Diagnostics) {
|
||||
_, keyType := GetTraverserKey(traverser)
|
||||
|
||||
var diagnostics hcl.Diagnostics
|
||||
if !InputType(StringType).ConversionFrom(keyType).Exists() {
|
||||
diagnostics = hcl.Diagnostics{unsupportedMapKey(traverser.SourceRange())}
|
||||
}
|
||||
return t.ElementType, diagnostics
|
||||
}
|
||||
|
||||
// SyntaxNode returns the syntax node for the type. This is always syntax.None.
|
||||
func (*MapType) SyntaxNode() hclsyntax.Node {
|
||||
return syntax.None
|
||||
}
|
||||
|
||||
// AssignableFrom returns true if this type is assignable from the indicated source type. A map(T) is assignable
|
||||
// from values of type map(U) where T is assignable from U or object(K_0=U_0, ..., K_N=U_N) if T is assignable from the
|
||||
// unified type of U_0 through U_N.
|
||||
func (t *MapType) AssignableFrom(src Type) bool {
|
||||
return assignableFrom(t, src, func() bool {
|
||||
switch src := src.(type) {
|
||||
case *MapType:
|
||||
return t.ElementType.AssignableFrom(src.ElementType)
|
||||
case *ObjectType:
|
||||
for _, src := range src.Properties {
|
||||
if !t.ElementType.AssignableFrom(src) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
func (t *MapType) ConversionFrom(src Type) ConversionKind {
|
||||
return t.conversionFrom(src, false)
|
||||
}
|
||||
|
||||
func (t *MapType) conversionFrom(src Type, unifying bool) ConversionKind {
|
||||
return conversionFrom(t, src, unifying, func() ConversionKind {
|
||||
switch src := src.(type) {
|
||||
case *MapType:
|
||||
return t.ElementType.conversionFrom(src.ElementType, unifying)
|
||||
case *ObjectType:
|
||||
conversionKind := SafeConversion
|
||||
for _, src := range src.Properties {
|
||||
if ck := t.ElementType.conversionFrom(src, unifying); ck < conversionKind {
|
||||
conversionKind = ck
|
||||
}
|
||||
}
|
||||
return conversionKind
|
||||
}
|
||||
return NoConversion
|
||||
})
|
||||
}
|
||||
|
||||
func (t *MapType) String() string {
|
||||
if t.s == "" {
|
||||
t.s = fmt.Sprintf("map(%v)", t.ElementType)
|
||||
}
|
||||
return t.s
|
||||
}
|
||||
|
||||
func (t *MapType) unify(other Type) (Type, ConversionKind) {
|
||||
return unify(t, other, func() (Type, ConversionKind) {
|
||||
switch other := other.(type) {
|
||||
case *MapType:
|
||||
// If the other type is a map type, unify based on the element type.
|
||||
elementType, conversionKind := t.ElementType.unify(other.ElementType)
|
||||
return NewMapType(elementType), conversionKind
|
||||
case *ObjectType:
|
||||
// If the other type is an object type, prefer the map type, but unify the property types.
|
||||
elementType, conversionKind := t.ElementType, SafeConversion
|
||||
for _, other := range other.Properties {
|
||||
element, ck := elementType.unify(other)
|
||||
if ck < conversionKind {
|
||||
conversionKind = ck
|
||||
}
|
||||
elementType = element
|
||||
}
|
||||
return NewMapType(elementType), conversionKind
|
||||
default:
|
||||
// Prefer the map type.
|
||||
return t, t.conversionFrom(other, true)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (*MapType) isType() {}
|
59
pkg/codegen/hcl2/model/type_none.go
Normal file
59
pkg/codegen/hcl2/model/type_none.go
Normal file
|
@ -0,0 +1,59 @@
|
|||
// 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 model
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||
"github.com/pulumi/pulumi/pkg/codegen/hcl2/syntax"
|
||||
)
|
||||
|
||||
type noneType int
|
||||
|
||||
func (noneType) SyntaxNode() hclsyntax.Node {
|
||||
return syntax.None
|
||||
}
|
||||
|
||||
func (noneType) Traverse(traverser hcl.Traverser) (Traversable, hcl.Diagnostics) {
|
||||
return NoneType, hcl.Diagnostics{unsupportedReceiverType(NoneType, traverser.SourceRange())}
|
||||
}
|
||||
|
||||
func (noneType) AssignableFrom(src Type) bool {
|
||||
return assignableFrom(NoneType, src, func() bool {
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
func (noneType) ConversionFrom(src Type) ConversionKind {
|
||||
return NoneType.conversionFrom(src, false)
|
||||
}
|
||||
|
||||
func (noneType) conversionFrom(src Type, unifying bool) ConversionKind {
|
||||
return conversionFrom(NoneType, src, unifying, func() ConversionKind {
|
||||
return NoConversion
|
||||
})
|
||||
}
|
||||
|
||||
func (noneType) String() string {
|
||||
return "none"
|
||||
}
|
||||
|
||||
func (noneType) unify(other Type) (Type, ConversionKind) {
|
||||
return unify(NoneType, other, func() (Type, ConversionKind) {
|
||||
return NoneType, other.ConversionFrom(NoneType)
|
||||
})
|
||||
}
|
||||
|
||||
func (noneType) isType() {}
|
224
pkg/codegen/hcl2/model/type_object.go
Normal file
224
pkg/codegen/hcl2/model/type_object.go
Normal file
|
@ -0,0 +1,224 @@
|
|||
// 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 model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||
"github.com/pulumi/pulumi/pkg/codegen/hcl2/syntax"
|
||||
"github.com/pulumi/pulumi/sdk/go/common/util/contract"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
"github.com/zclconf/go-cty/cty/convert"
|
||||
)
|
||||
|
||||
// ObjectType represents schematized maps from strings to particular types.
|
||||
type ObjectType struct {
|
||||
// Properties records the types of the object's properties.
|
||||
Properties map[string]Type
|
||||
|
||||
propertyUnion Type
|
||||
s string
|
||||
}
|
||||
|
||||
// The set of object types, indexed by string representation.
|
||||
var objectTypes = map[string]*ObjectType{}
|
||||
|
||||
// NewObjectType creates a new object type with the given properties.
|
||||
func NewObjectType(properties map[string]Type) *ObjectType {
|
||||
t := &ObjectType{Properties: properties}
|
||||
if t, ok := objectTypes[t.String()]; ok {
|
||||
return t
|
||||
}
|
||||
objectTypes[t.String()] = t
|
||||
return t
|
||||
}
|
||||
|
||||
// SyntaxNode returns the syntax node for the type. This is always syntax.None.
|
||||
func (*ObjectType) SyntaxNode() hclsyntax.Node {
|
||||
return syntax.None
|
||||
}
|
||||
|
||||
// Traverse attempts to traverse the optional type with the given traverser. The result type of
|
||||
// traverse(object({K_0 = T_0, ..., K_N = T_N})) is T_i if the traverser is the string literal K_i. If the traverser is
|
||||
// a string but not a literal, the result type is any.
|
||||
func (t *ObjectType) Traverse(traverser hcl.Traverser) (Traversable, hcl.Diagnostics) {
|
||||
key, keyType := GetTraverserKey(traverser)
|
||||
|
||||
if !InputType(StringType).ConversionFrom(keyType).Exists() {
|
||||
return DynamicType, hcl.Diagnostics{unsupportedObjectProperty(traverser.SourceRange())}
|
||||
}
|
||||
|
||||
if key == cty.DynamicVal {
|
||||
if t.propertyUnion == nil {
|
||||
types := make([]Type, 0, len(t.Properties))
|
||||
for _, t := range t.Properties {
|
||||
types = append(types, t)
|
||||
}
|
||||
t.propertyUnion = NewUnionType(types...)
|
||||
}
|
||||
return t.propertyUnion, nil
|
||||
}
|
||||
|
||||
keyString, err := convert.Convert(key, cty.String)
|
||||
contract.Assert(err == nil)
|
||||
|
||||
propertyName := keyString.AsString()
|
||||
propertyType, hasProperty := t.Properties[propertyName]
|
||||
if !hasProperty {
|
||||
return DynamicType, hcl.Diagnostics{unknownObjectProperty(propertyName, traverser.SourceRange())}
|
||||
}
|
||||
return propertyType, nil
|
||||
}
|
||||
|
||||
// AssignableFrom returns true if this type is assignable from the indicated source type.
|
||||
// An object({K_0 = T_0, ..., K_N = T_N}) is assignable from U = object({K_0 = U_0, ... K_M = U_M}), where T_I is
|
||||
// assignable from U[K_I] for all I from 0 to N.
|
||||
func (t *ObjectType) AssignableFrom(src Type) bool {
|
||||
return assignableFrom(t, src, func() bool {
|
||||
if src, ok := src.(*ObjectType); ok {
|
||||
for key, t := range t.Properties {
|
||||
src, ok := src.Properties[key]
|
||||
if !ok {
|
||||
src = NoneType
|
||||
}
|
||||
if !t.AssignableFrom(src) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
type objectTypeUnifier struct {
|
||||
properties map[string]Type
|
||||
any bool
|
||||
conversionKind ConversionKind
|
||||
}
|
||||
|
||||
func (u *objectTypeUnifier) unify(t *ObjectType) {
|
||||
if !u.any {
|
||||
u.properties = map[string]Type{}
|
||||
for k, t := range t.Properties {
|
||||
u.properties[k] = t
|
||||
}
|
||||
u.any, u.conversionKind = true, SafeConversion
|
||||
} else {
|
||||
for key, pt := range u.properties {
|
||||
if _, exists := t.Properties[key]; !exists {
|
||||
u.properties[key] = NewOptionalType(pt)
|
||||
}
|
||||
}
|
||||
|
||||
for key, t := range t.Properties {
|
||||
if pt, exists := u.properties[key]; exists {
|
||||
unified, ck := pt.unify(t)
|
||||
if ck < u.conversionKind {
|
||||
u.conversionKind = ck
|
||||
}
|
||||
u.properties[key] = unified
|
||||
} else {
|
||||
u.properties[key] = NewOptionalType(t)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *ObjectType) ConversionFrom(src Type) ConversionKind {
|
||||
return t.conversionFrom(src, false)
|
||||
}
|
||||
|
||||
func (t *ObjectType) conversionFrom(src Type, unifying bool) ConversionKind {
|
||||
return conversionFrom(t, src, unifying, func() ConversionKind {
|
||||
switch src := src.(type) {
|
||||
case *ObjectType:
|
||||
if unifying {
|
||||
var unifier objectTypeUnifier
|
||||
unifier.unify(t)
|
||||
unifier.unify(src)
|
||||
return unifier.conversionKind
|
||||
}
|
||||
|
||||
conversionKind := SafeConversion
|
||||
for k, dst := range t.Properties {
|
||||
src, ok := src.Properties[k]
|
||||
if !ok {
|
||||
src = NoneType
|
||||
}
|
||||
if ck := dst.conversionFrom(src, unifying); ck < conversionKind {
|
||||
conversionKind = ck
|
||||
}
|
||||
}
|
||||
return conversionKind
|
||||
case *MapType:
|
||||
conversionKind := UnsafeConversion
|
||||
for _, dst := range t.Properties {
|
||||
if ck := dst.conversionFrom(src.ElementType, unifying); ck < conversionKind {
|
||||
conversionKind = ck
|
||||
}
|
||||
}
|
||||
return conversionKind
|
||||
}
|
||||
return NoConversion
|
||||
})
|
||||
}
|
||||
|
||||
func (t *ObjectType) String() string {
|
||||
if t.s == "" {
|
||||
var properties []string
|
||||
for k, v := range t.Properties {
|
||||
properties = append(properties, fmt.Sprintf("%s = %v", k, v))
|
||||
}
|
||||
sort.Strings(properties)
|
||||
|
||||
t.s = fmt.Sprintf("object({%s})", strings.Join(properties, ", "))
|
||||
}
|
||||
return t.s
|
||||
}
|
||||
|
||||
func (t *ObjectType) unify(other Type) (Type, ConversionKind) {
|
||||
return unify(t, other, func() (Type, ConversionKind) {
|
||||
switch other := other.(type) {
|
||||
case *MapType:
|
||||
// Prefer the map type, but unify the element type.
|
||||
elementType, conversionKind := other.ElementType, SafeConversion
|
||||
for _, t := range t.Properties {
|
||||
element, ck := elementType.unify(t)
|
||||
if ck < conversionKind {
|
||||
conversionKind = ck
|
||||
}
|
||||
elementType = element
|
||||
}
|
||||
return NewMapType(elementType), conversionKind
|
||||
case *ObjectType:
|
||||
// If the other type is an object type, produce a new type whose properties are the union of the two types.
|
||||
// The types of intersecting properties will be unified.
|
||||
var unifier objectTypeUnifier
|
||||
unifier.unify(t)
|
||||
unifier.unify(other)
|
||||
return NewObjectType(unifier.properties), unifier.conversionKind
|
||||
default:
|
||||
// Otherwise, prefer the object type.
|
||||
return t, t.conversionFrom(other, true)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (*ObjectType) isType() {}
|
168
pkg/codegen/hcl2/model/type_opaque.go
Normal file
168
pkg/codegen/hcl2/model/type_opaque.go
Normal file
|
@ -0,0 +1,168 @@
|
|||
// 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 model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/pulumi/pulumi/pkg/codegen/hcl2/syntax"
|
||||
"github.com/pulumi/pulumi/sdk/go/common/util/contract"
|
||||
)
|
||||
|
||||
// OpaqueType represents a type that is named by a string.
|
||||
type OpaqueType struct {
|
||||
// Name is the type's name.
|
||||
Name string
|
||||
|
||||
s string
|
||||
}
|
||||
|
||||
// The set of opaque types, indexed by name.
|
||||
var opaqueTypes = map[string]*OpaqueType{}
|
||||
|
||||
// GetOpaqueType fetches the opaque type for the given name.
|
||||
func GetOpaqueType(name string) (*OpaqueType, bool) {
|
||||
t, ok := opaqueTypes[name]
|
||||
return t, ok
|
||||
}
|
||||
|
||||
// MustNewOpaqueType creates a new opaque type with the given name.
|
||||
func MustNewOpaqueType(name string) *OpaqueType {
|
||||
t, err := NewOpaqueType(name)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// NewOpaqueType creates a new opaque type with the given name.
|
||||
func NewOpaqueType(name string) (*OpaqueType, error) {
|
||||
if _, ok := opaqueTypes[name]; ok {
|
||||
return nil, errors.Errorf("opaque type %s is already defined", name)
|
||||
}
|
||||
|
||||
t := &OpaqueType{Name: name}
|
||||
opaqueTypes[name] = t
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// SyntaxNode returns the syntax node for the type. This is always syntax.None.
|
||||
func (*OpaqueType) SyntaxNode() hclsyntax.Node {
|
||||
return syntax.None
|
||||
}
|
||||
|
||||
// Traverse attempts to traverse the opaque type with the given traverser. The result type of traverse(opaque(name))
|
||||
// is dynamic if name is "dynamic"; otherwise the traversal fails.
|
||||
func (t *OpaqueType) Traverse(traverser hcl.Traverser) (Traversable, hcl.Diagnostics) {
|
||||
if t == DynamicType {
|
||||
return DynamicType, nil
|
||||
}
|
||||
|
||||
return DynamicType, hcl.Diagnostics{unsupportedReceiverType(t, traverser.SourceRange())}
|
||||
}
|
||||
|
||||
// AssignableFrom returns true if this type is assignable from the indicated source type. A token(name) is assignable
|
||||
// from token(name).
|
||||
func (t *OpaqueType) AssignableFrom(src Type) bool {
|
||||
return assignableFrom(t, src, func() bool {
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
func (t *OpaqueType) conversionFromImpl(src Type, unifying, checkUnsafe bool) ConversionKind {
|
||||
return conversionFrom(t, src, unifying, func() ConversionKind {
|
||||
switch {
|
||||
case t == NumberType:
|
||||
// src == NumberType is handled by t == src above
|
||||
contract.Assert(src != NumberType)
|
||||
|
||||
cki := IntType.conversionFromImpl(src, unifying, false)
|
||||
if cki == SafeConversion {
|
||||
return SafeConversion
|
||||
}
|
||||
if cki == UnsafeConversion || checkUnsafe && StringType.conversionFromImpl(src, unifying, false).Exists() {
|
||||
return UnsafeConversion
|
||||
}
|
||||
return NoConversion
|
||||
case t == IntType:
|
||||
if checkUnsafe && NumberType.conversionFromImpl(src, unifying, true).Exists() {
|
||||
return UnsafeConversion
|
||||
}
|
||||
return NoConversion
|
||||
case t == BoolType:
|
||||
if checkUnsafe && StringType.conversionFromImpl(src, unifying, false).Exists() {
|
||||
return UnsafeConversion
|
||||
}
|
||||
return NoConversion
|
||||
case t == StringType:
|
||||
ckb := BoolType.conversionFromImpl(src, unifying, false)
|
||||
ckn := NumberType.conversionFromImpl(src, unifying, false)
|
||||
if ckb == SafeConversion || ckn == SafeConversion {
|
||||
return SafeConversion
|
||||
}
|
||||
if ckb == UnsafeConversion || ckn == UnsafeConversion {
|
||||
return UnsafeConversion
|
||||
}
|
||||
return NoConversion
|
||||
default:
|
||||
return NoConversion
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (t *OpaqueType) conversionFrom(src Type, unifying bool) ConversionKind {
|
||||
return t.conversionFromImpl(src, unifying, true)
|
||||
}
|
||||
|
||||
func (t *OpaqueType) ConversionFrom(src Type) ConversionKind {
|
||||
return t.conversionFrom(src, false)
|
||||
}
|
||||
|
||||
func (t *OpaqueType) String() string {
|
||||
if t.s == "" {
|
||||
t.s = fmt.Sprintf("opaque(%s)", t.Name)
|
||||
}
|
||||
return t.s
|
||||
}
|
||||
|
||||
var opaquePrecedence = []*OpaqueType{StringType, NumberType, IntType, BoolType}
|
||||
|
||||
func (t *OpaqueType) unify(other Type) (Type, ConversionKind) {
|
||||
return unify(t, other, func() (Type, ConversionKind) {
|
||||
if t == DynamicType || other == DynamicType {
|
||||
// These should have been handled by unify.
|
||||
contract.Failf("unexpected type %v in OpaqueType.unify", t)
|
||||
return DynamicType, SafeConversion
|
||||
}
|
||||
|
||||
for _, goal := range opaquePrecedence {
|
||||
if t == goal {
|
||||
return goal, goal.conversionFrom(other, true)
|
||||
}
|
||||
if other == goal {
|
||||
return goal, goal.conversionFrom(t, true)
|
||||
}
|
||||
}
|
||||
|
||||
// There should be a total order on conversions to and from these types, so there should be a total order
|
||||
// on unifications with these types.
|
||||
return DynamicType, SafeConversion
|
||||
})
|
||||
}
|
||||
|
||||
func (*OpaqueType) isType() {}
|
116
pkg/codegen/hcl2/model/type_output.go
Normal file
116
pkg/codegen/hcl2/model/type_output.go
Normal file
|
@ -0,0 +1,116 @@
|
|||
// 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 model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||
"github.com/pulumi/pulumi/pkg/codegen/hcl2/syntax"
|
||||
)
|
||||
|
||||
// OutputType represents eventual values that carry additional application-specific information.
|
||||
type OutputType struct {
|
||||
// ElementType is the element type of the output.
|
||||
ElementType Type
|
||||
|
||||
s string
|
||||
}
|
||||
|
||||
// The set of output types, indexed by element type.
|
||||
var outputTypes = map[Type]*OutputType{}
|
||||
|
||||
// NewOutputType creates a new output type with the given element type after replacing any output or promise types
|
||||
// within the element type with their respective element types.
|
||||
func NewOutputType(elementType Type) *OutputType {
|
||||
elementType = ResolveOutputs(elementType)
|
||||
if t, ok := outputTypes[elementType]; ok {
|
||||
return t
|
||||
}
|
||||
|
||||
t := &OutputType{ElementType: elementType}
|
||||
outputTypes[elementType] = t
|
||||
return t
|
||||
}
|
||||
|
||||
// SyntaxNode returns the syntax node for the type. This is always syntax.None.
|
||||
func (*OutputType) SyntaxNode() hclsyntax.Node {
|
||||
return syntax.None
|
||||
}
|
||||
|
||||
// Traverse attempts to traverse the output type with the given traverser. The result type of traverse(output(T))
|
||||
// is output(traverse(T)).
|
||||
func (t *OutputType) Traverse(traverser hcl.Traverser) (Traversable, hcl.Diagnostics) {
|
||||
element, diagnostics := t.ElementType.Traverse(traverser)
|
||||
return NewOutputType(element.(Type)), diagnostics
|
||||
}
|
||||
|
||||
// AssignableFrom returns true if this type is assignable from the indicated source type. An output(T) is assignable
|
||||
// from values of type output(U), promise(U), and U, where T is assignable from U.
|
||||
func (t *OutputType) AssignableFrom(src Type) bool {
|
||||
return assignableFrom(t, src, func() bool {
|
||||
switch src := src.(type) {
|
||||
case *OutputType:
|
||||
return t.ElementType.AssignableFrom(src.ElementType)
|
||||
case *PromiseType:
|
||||
return t.ElementType.AssignableFrom(src.ElementType)
|
||||
}
|
||||
return t.ElementType.AssignableFrom(src)
|
||||
})
|
||||
}
|
||||
|
||||
func (t *OutputType) ConversionFrom(src Type) ConversionKind {
|
||||
return t.conversionFrom(src, false)
|
||||
}
|
||||
|
||||
func (t *OutputType) conversionFrom(src Type, unifying bool) ConversionKind {
|
||||
return conversionFrom(t, src, unifying, func() ConversionKind {
|
||||
switch src := src.(type) {
|
||||
case *OutputType:
|
||||
return t.ElementType.conversionFrom(src.ElementType, unifying)
|
||||
case *PromiseType:
|
||||
return t.ElementType.conversionFrom(src.ElementType, unifying)
|
||||
}
|
||||
return t.ElementType.conversionFrom(src, unifying)
|
||||
})
|
||||
}
|
||||
|
||||
func (t *OutputType) String() string {
|
||||
if t.s == "" {
|
||||
t.s = fmt.Sprintf("output(%v)", t.ElementType)
|
||||
}
|
||||
return t.s
|
||||
}
|
||||
|
||||
func (t *OutputType) unify(other Type) (Type, ConversionKind) {
|
||||
return unify(t, other, func() (Type, ConversionKind) {
|
||||
switch other := other.(type) {
|
||||
case *OutputType:
|
||||
// If the other type is an output type, unify based on the element type.
|
||||
elementType, conversionKind := t.ElementType.unify(other.ElementType)
|
||||
return NewOutputType(elementType), conversionKind
|
||||
case *PromiseType:
|
||||
// If the other type is a promise type, unify based on the element type.
|
||||
elementType, conversionKind := t.ElementType.unify(other.ElementType)
|
||||
return NewOutputType(elementType), conversionKind
|
||||
default:
|
||||
// Prefer the output type.
|
||||
return t, t.conversionFrom(other, true)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (t *OutputType) isType() {}
|
110
pkg/codegen/hcl2/model/type_promise.go
Normal file
110
pkg/codegen/hcl2/model/type_promise.go
Normal file
|
@ -0,0 +1,110 @@
|
|||
// 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 model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||
"github.com/pulumi/pulumi/pkg/codegen/hcl2/syntax"
|
||||
)
|
||||
|
||||
// PromiseType represents eventual values that do not carry additional information.
|
||||
type PromiseType struct {
|
||||
// ElementType is the element type of the promise.
|
||||
ElementType Type
|
||||
|
||||
s string
|
||||
}
|
||||
|
||||
// The set of promise types, indexed by element type.
|
||||
var promiseTypes = map[Type]*PromiseType{}
|
||||
|
||||
// NewPromiseType creates a new promise type with the given element type after replacing any promise types within
|
||||
// the element type with their respective element types.
|
||||
func NewPromiseType(elementType Type) *PromiseType {
|
||||
elementType = ResolvePromises(elementType)
|
||||
if t, ok := promiseTypes[elementType]; ok {
|
||||
return t
|
||||
}
|
||||
|
||||
t := &PromiseType{ElementType: elementType}
|
||||
promiseTypes[elementType] = t
|
||||
return t
|
||||
}
|
||||
|
||||
// SyntaxNode returns the syntax node for the type. This is always syntax.None.
|
||||
func (*PromiseType) SyntaxNode() hclsyntax.Node {
|
||||
return syntax.None
|
||||
}
|
||||
|
||||
// Traverse attempts to traverse the promise type with the given traverser. The result type of traverse(promise(T))
|
||||
// is promise(traverse(T)).
|
||||
func (t *PromiseType) Traverse(traverser hcl.Traverser) (Traversable, hcl.Diagnostics) {
|
||||
element, diagnostics := t.ElementType.Traverse(traverser)
|
||||
return NewPromiseType(element.(Type)), diagnostics
|
||||
}
|
||||
|
||||
// AssignableFrom returns true if this type is assignable from the indicated source type. A promise(T) is assignable
|
||||
// from values of type promise(U) and U, where T is assignable from U.
|
||||
func (t *PromiseType) AssignableFrom(src Type) bool {
|
||||
return assignableFrom(t, src, func() bool {
|
||||
if src, ok := src.(*PromiseType); ok {
|
||||
return t.ElementType.AssignableFrom(src.ElementType)
|
||||
}
|
||||
return t.ElementType.AssignableFrom(src)
|
||||
})
|
||||
}
|
||||
|
||||
func (t *PromiseType) ConversionFrom(src Type) ConversionKind {
|
||||
return t.conversionFrom(src, false)
|
||||
}
|
||||
|
||||
func (t *PromiseType) conversionFrom(src Type, unifying bool) ConversionKind {
|
||||
return conversionFrom(t, src, unifying, func() ConversionKind {
|
||||
if src, ok := src.(*PromiseType); ok {
|
||||
return t.ElementType.conversionFrom(src.ElementType, unifying)
|
||||
}
|
||||
return t.ElementType.conversionFrom(src, unifying)
|
||||
})
|
||||
}
|
||||
|
||||
func (t *PromiseType) String() string {
|
||||
if t.s == "" {
|
||||
t.s = fmt.Sprintf("promise(%v)", t.ElementType)
|
||||
}
|
||||
return t.s
|
||||
}
|
||||
|
||||
func (t *PromiseType) unify(other Type) (Type, ConversionKind) {
|
||||
return unify(t, other, func() (Type, ConversionKind) {
|
||||
switch other := other.(type) {
|
||||
case *PromiseType:
|
||||
// If the other type is a promise type, unify based on the element type.
|
||||
elementType, conversionKind := t.ElementType.unify(other.ElementType)
|
||||
return NewPromiseType(elementType), conversionKind
|
||||
case *OutputType:
|
||||
// If the other type is an output type, prefer the optional type, but unify the element type.
|
||||
elementType, conversionKind := t.unify(other.ElementType)
|
||||
return NewOutputType(elementType), conversionKind
|
||||
default:
|
||||
// Prefer the promise type.
|
||||
return t, t.conversionFrom(other, true)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (t *PromiseType) isType() {}
|
128
pkg/codegen/hcl2/model/type_set.go
Normal file
128
pkg/codegen/hcl2/model/type_set.go
Normal file
|
@ -0,0 +1,128 @@
|
|||
// 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 model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||
"github.com/pulumi/pulumi/pkg/codegen/hcl2/syntax"
|
||||
)
|
||||
|
||||
// SetType represents sets of particular element types.
|
||||
type SetType struct {
|
||||
// ElementType is the element type of the set.
|
||||
ElementType Type
|
||||
|
||||
s string
|
||||
}
|
||||
|
||||
// The set of set types, indexed by element type.
|
||||
var setTypes = map[Type]*SetType{}
|
||||
|
||||
// NewSetType creates a new set type with the given element type.
|
||||
func NewSetType(elementType Type) *SetType {
|
||||
if t, ok := setTypes[elementType]; ok {
|
||||
return t
|
||||
}
|
||||
|
||||
t := &SetType{ElementType: elementType}
|
||||
setTypes[elementType] = t
|
||||
return t
|
||||
}
|
||||
|
||||
// SyntaxNode returns the syntax node for the type. This is always syntax.None.
|
||||
func (*SetType) SyntaxNode() hclsyntax.Node {
|
||||
return syntax.None
|
||||
}
|
||||
|
||||
// Traverse attempts to traverse the optional type with the given traverser. This always fails.
|
||||
func (t *SetType) Traverse(traverser hcl.Traverser) (Traversable, hcl.Diagnostics) {
|
||||
return DynamicType, hcl.Diagnostics{unsupportedReceiverType(t, traverser.SourceRange())}
|
||||
}
|
||||
|
||||
// AssignableFrom returns true if this type is assignable from the indicated source type. A set(T) is assignable
|
||||
// from values of type set(U) where T is assignable from U.
|
||||
func (t *SetType) AssignableFrom(src Type) bool {
|
||||
return assignableFrom(t, src, func() bool {
|
||||
if src, ok := src.(*SetType); ok {
|
||||
return t.ElementType.AssignableFrom(src.ElementType)
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
func (t *SetType) ConversionFrom(src Type) ConversionKind {
|
||||
return t.conversionFrom(src, false)
|
||||
}
|
||||
|
||||
func (t *SetType) conversionFrom(src Type, unifying bool) ConversionKind {
|
||||
return conversionFrom(t, src, unifying, func() ConversionKind {
|
||||
switch src := src.(type) {
|
||||
case *SetType:
|
||||
return t.ElementType.conversionFrom(src.ElementType, unifying)
|
||||
case *ListType:
|
||||
if conversionKind := t.ElementType.conversionFrom(src.ElementType, unifying); conversionKind == NoConversion {
|
||||
return NoConversion
|
||||
}
|
||||
return UnsafeConversion
|
||||
case *TupleType:
|
||||
if conversionKind := NewListType(t.ElementType).conversionFrom(src, unifying); conversionKind == NoConversion {
|
||||
return NoConversion
|
||||
}
|
||||
return UnsafeConversion
|
||||
}
|
||||
return NoConversion
|
||||
})
|
||||
}
|
||||
|
||||
func (t *SetType) String() string {
|
||||
if t.s == "" {
|
||||
t.s = fmt.Sprintf("set(%v)", t.ElementType)
|
||||
}
|
||||
return t.s
|
||||
}
|
||||
|
||||
func (t *SetType) unify(other Type) (Type, ConversionKind) {
|
||||
return unify(t, other, func() (Type, ConversionKind) {
|
||||
switch other := other.(type) {
|
||||
case *SetType:
|
||||
// If the other type is a set type, unify based on the element type.
|
||||
elementType, conversionKind := t.ElementType.unify(other.ElementType)
|
||||
return NewSetType(elementType), conversionKind
|
||||
case *ListType:
|
||||
// Prefer the list type, but unify the element types.
|
||||
element, conversionKind := t.ElementType.unify(other.ElementType)
|
||||
return NewListType(element), conversionKind
|
||||
case *TupleType:
|
||||
// Prefer the set type, but unify the element type.
|
||||
elementType, conversionKind := t.ElementType, UnsafeConversion
|
||||
for _, other := range other.ElementTypes {
|
||||
element, ck := elementType.unify(other)
|
||||
if ck < conversionKind {
|
||||
conversionKind = ck
|
||||
}
|
||||
elementType = element
|
||||
}
|
||||
return NewSetType(elementType), conversionKind
|
||||
default:
|
||||
// Prefer the set type.
|
||||
return t, t.conversionFrom(other, true)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (*SetType) isType() {}
|
647
pkg/codegen/hcl2/model/type_test.go
Normal file
647
pkg/codegen/hcl2/model/type_test.go
Normal file
|
@ -0,0 +1,647 @@
|
|||
// 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 model
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
func testTraverse(t *testing.T, receiver Traversable, traverser hcl.Traverser, expected Traversable, expectDiags bool) {
|
||||
actual, diags := receiver.Traverse(traverser)
|
||||
assert.Equal(t, expected, actual)
|
||||
if expectDiags {
|
||||
assert.Greater(t, len(diags), 0)
|
||||
} else {
|
||||
assert.Equal(t, 0, len(diags))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDynamicType(t *testing.T) {
|
||||
// Test that DynamicType is assignable to and from itself.
|
||||
assert.True(t, DynamicType.AssignableFrom(DynamicType))
|
||||
|
||||
// Test that DynamicType is assignable from any type.
|
||||
assert.True(t, DynamicType.AssignableFrom(BoolType))
|
||||
assert.True(t, DynamicType.AssignableFrom(IntType))
|
||||
assert.True(t, DynamicType.AssignableFrom(NumberType))
|
||||
assert.True(t, DynamicType.AssignableFrom(StringType))
|
||||
|
||||
assert.True(t, DynamicType.AssignableFrom(NewOptionalType(BoolType)))
|
||||
assert.True(t, DynamicType.AssignableFrom(NewOutputType(BoolType)))
|
||||
assert.True(t, DynamicType.AssignableFrom(NewPromiseType(BoolType)))
|
||||
assert.True(t, DynamicType.AssignableFrom(NewMapType(BoolType)))
|
||||
assert.True(t, DynamicType.AssignableFrom(NewListType(BoolType)))
|
||||
assert.True(t, DynamicType.AssignableFrom(NewUnionType(BoolType, IntType)))
|
||||
assert.True(t, DynamicType.AssignableFrom(NewObjectType(map[string]Type{
|
||||
"bool": BoolType,
|
||||
"int": IntType,
|
||||
})))
|
||||
|
||||
// Test that DynamicType is convertible from any type.
|
||||
assert.True(t, DynamicType.ConversionFrom(BoolType).Exists())
|
||||
assert.True(t, DynamicType.ConversionFrom(IntType).Exists())
|
||||
assert.True(t, DynamicType.ConversionFrom(NumberType).Exists())
|
||||
assert.True(t, DynamicType.ConversionFrom(StringType).Exists())
|
||||
|
||||
assert.True(t, DynamicType.ConversionFrom(NewOptionalType(BoolType)).Exists())
|
||||
assert.True(t, DynamicType.ConversionFrom(NewOutputType(BoolType)).Exists())
|
||||
assert.True(t, DynamicType.ConversionFrom(NewPromiseType(BoolType)).Exists())
|
||||
assert.True(t, DynamicType.ConversionFrom(NewMapType(BoolType)).Exists())
|
||||
assert.True(t, DynamicType.ConversionFrom(NewListType(BoolType)).Exists())
|
||||
assert.True(t, DynamicType.ConversionFrom(NewUnionType(BoolType, IntType)).Exists())
|
||||
assert.True(t, DynamicType.ConversionFrom(NewObjectType(map[string]Type{
|
||||
"bool": BoolType,
|
||||
"int": IntType,
|
||||
})).Exists())
|
||||
|
||||
// Test that DynamicType is convertible to any type.
|
||||
assert.True(t, BoolType.ConversionFrom(DynamicType).Exists())
|
||||
assert.True(t, IntType.ConversionFrom(DynamicType).Exists())
|
||||
assert.True(t, NumberType.ConversionFrom(DynamicType).Exists())
|
||||
assert.True(t, StringType.ConversionFrom(DynamicType).Exists())
|
||||
|
||||
assert.True(t, NewOptionalType(BoolType).ConversionFrom(DynamicType).Exists())
|
||||
assert.True(t, NewOutputType(BoolType).ConversionFrom(DynamicType).Exists())
|
||||
assert.True(t, NewPromiseType(BoolType).ConversionFrom(DynamicType).Exists())
|
||||
assert.True(t, NewMapType(BoolType).ConversionFrom(DynamicType).Exists())
|
||||
assert.True(t, NewListType(BoolType).ConversionFrom(DynamicType).Exists())
|
||||
assert.True(t, NewUnionType(BoolType, IntType).ConversionFrom(DynamicType).Exists())
|
||||
assert.True(t, NewObjectType(map[string]Type{
|
||||
"bool": BoolType,
|
||||
"int": IntType,
|
||||
}).ConversionFrom(DynamicType).Exists())
|
||||
|
||||
// Test that traversals on DynamicType always succeed.
|
||||
testTraverse(t, DynamicType, hcl.TraverseAttr{Name: "foo"}, DynamicType, false)
|
||||
testTraverse(t, DynamicType, hcl.TraverseIndex{Key: cty.StringVal("foo")}, DynamicType, false)
|
||||
testTraverse(t, DynamicType, hcl.TraverseIndex{Key: cty.NumberIntVal(0)}, DynamicType, false)
|
||||
testTraverse(t, DynamicType, hcl.TraverseIndex{Key: encapsulateType(DynamicType)}, DynamicType, false)
|
||||
}
|
||||
|
||||
func TestOptionalType(t *testing.T) {
|
||||
typ := NewOptionalType(DynamicType)
|
||||
|
||||
// Test that creating an optional type with the same element type does not create a new type.
|
||||
typ2 := NewOptionalType(DynamicType)
|
||||
assert.EqualValues(t, typ, typ2)
|
||||
|
||||
// Test that creating an optional type with an element type that is also optional does not create a new type.
|
||||
typ2 = NewOptionalType(typ)
|
||||
assert.Equal(t, typ, typ2)
|
||||
|
||||
// Test that an optional type is assignable to and from itself.
|
||||
assert.True(t, typ.AssignableFrom(typ))
|
||||
|
||||
// Test that an optional type is assignable from none.
|
||||
assert.True(t, typ.AssignableFrom(NoneType))
|
||||
|
||||
// Test that an optional type is assignable from its element type.
|
||||
assert.True(t, NewOptionalType(StringType).AssignableFrom(StringType))
|
||||
|
||||
// Test that an optional(T) is assignable from an U, where U is assignable to T.
|
||||
assert.True(t, typ.AssignableFrom(BoolType))
|
||||
|
||||
// Test that an optional(T) is assignable from an optional(U), where U is assignable to T.
|
||||
assert.True(t, typ.AssignableFrom(NewOptionalType(BoolType)))
|
||||
|
||||
// Test that traversing an optional(T) returns an optional(U), where U is the result of the inner traversal.
|
||||
typ = NewOptionalType(NewMapType(StringType))
|
||||
testTraverse(t, typ, hcl.TraverseAttr{Name: "foo"}, NewOptionalType(StringType), false)
|
||||
testTraverse(t, typ, hcl.TraverseIndex{Key: cty.StringVal("foo")}, NewOptionalType(StringType), false)
|
||||
testTraverse(t, typ, hcl.TraverseIndex{Key: cty.NumberIntVal(0)}, NewOptionalType(StringType), false)
|
||||
testTraverse(t, typ, hcl.TraverseIndex{Key: encapsulateType(IntType)}, NewOptionalType(StringType), false)
|
||||
}
|
||||
|
||||
func TestOutputType(t *testing.T) {
|
||||
typ := NewOutputType(DynamicType)
|
||||
|
||||
// Test that creating an output type with the same element type does not create a new type.
|
||||
typ2 := NewOutputType(typ.ElementType)
|
||||
assert.EqualValues(t, typ, typ2)
|
||||
|
||||
// Test that creating an output type with an element type that is also an output does not create a new type.
|
||||
typ2 = NewOutputType(typ)
|
||||
assert.Equal(t, typ, typ2)
|
||||
|
||||
// Test that an output type is assignable to and from itself.
|
||||
assert.True(t, typ.AssignableFrom(typ))
|
||||
|
||||
// Test that an output type is assignable from its element type.
|
||||
assert.True(t, NewOutputType(StringType).AssignableFrom(StringType))
|
||||
|
||||
// Test that output(T) is assignable from U, where U is assignable to T.
|
||||
assert.True(t, typ.AssignableFrom(BoolType))
|
||||
|
||||
// Test that output(T) is assignable from output(U), where U is assignable to T.
|
||||
assert.True(t, typ.AssignableFrom(NewOutputType(BoolType)))
|
||||
|
||||
// Test that output(T) is assignable from promise(U), where U is assignable to T.
|
||||
assert.True(t, typ.AssignableFrom(NewPromiseType(BoolType)))
|
||||
|
||||
// Test that output(T) is _not_ assignable from U, where U is not assignable to T.
|
||||
assert.False(t, NewOutputType(BoolType).AssignableFrom(IntType))
|
||||
|
||||
// Test that output(T) is _not_ assignable from output(U), where U is not assignable to T.
|
||||
assert.False(t, NewOutputType(BoolType).AssignableFrom(NewOutputType(IntType)))
|
||||
|
||||
// Test that output(T) is _not_ assignable from promise(U), where U is not assignable to T.
|
||||
assert.False(t, NewOutputType(BoolType).AssignableFrom(NewPromiseType(IntType)))
|
||||
|
||||
// Test that traversing an output(T) returns an output(U), where U is the result of the inner traversal.
|
||||
typ = NewOutputType(NewMapType(StringType))
|
||||
testTraverse(t, typ, hcl.TraverseAttr{Name: "foo"}, NewOutputType(StringType), false)
|
||||
testTraverse(t, typ, hcl.TraverseIndex{Key: cty.StringVal("foo")}, NewOutputType(StringType), false)
|
||||
testTraverse(t, typ, hcl.TraverseIndex{Key: cty.NumberIntVal(0)}, NewOutputType(StringType), false)
|
||||
testTraverse(t, typ, hcl.TraverseIndex{Key: encapsulateType(IntType)}, NewOutputType(StringType), false)
|
||||
|
||||
// Test that ResolveOutputs correctly handles nested outputs.
|
||||
assert.Equal(t, NewOptionalType(BoolType), ResolveOutputs(NewOptionalType(NewOutputType(BoolType))))
|
||||
assert.Equal(t, BoolType, ResolveOutputs(NewOutputType(BoolType)))
|
||||
assert.Equal(t, BoolType, ResolveOutputs(NewPromiseType(BoolType)))
|
||||
assert.Equal(t, NewMapType(BoolType), ResolveOutputs(NewMapType(NewOutputType(BoolType))))
|
||||
assert.Equal(t, NewListType(BoolType), ResolveOutputs(NewListType(NewOutputType(BoolType))))
|
||||
assert.Equal(t, NewUnionType(BoolType, IntType), ResolveOutputs(NewUnionType(NewOutputType(BoolType),
|
||||
NewOutputType(IntType))))
|
||||
assert.Equal(t, NewObjectType(map[string]Type{
|
||||
"bool": BoolType,
|
||||
"int": IntType,
|
||||
}), ResolveOutputs(NewObjectType(map[string]Type{
|
||||
"bool": NewOutputType(BoolType),
|
||||
"int": NewOutputType(IntType),
|
||||
})))
|
||||
|
||||
// Test that NewOutputType correctly handles nested outputs.
|
||||
assert.Equal(t, NewOutputType(NewOptionalType(BoolType)), NewOutputType(NewOptionalType(NewOutputType(BoolType))))
|
||||
assert.Equal(t, NewOutputType(BoolType), NewOutputType(NewOutputType(BoolType)))
|
||||
assert.Equal(t, NewOutputType(BoolType), NewOutputType(NewPromiseType(BoolType)))
|
||||
assert.Equal(t, NewOutputType(NewMapType(BoolType)), NewOutputType(NewMapType(NewOutputType(BoolType))))
|
||||
assert.Equal(t, NewOutputType(NewListType(BoolType)), NewOutputType(NewListType(NewOutputType(BoolType))))
|
||||
assert.Equal(t, NewOutputType(NewUnionType(BoolType, IntType)),
|
||||
NewOutputType(NewUnionType(NewOutputType(BoolType), NewOutputType(IntType))))
|
||||
assert.Equal(t, NewOutputType(NewObjectType(map[string]Type{
|
||||
"bool": BoolType,
|
||||
"int": IntType,
|
||||
})), NewOutputType(NewObjectType(map[string]Type{
|
||||
"bool": NewOutputType(BoolType),
|
||||
"int": NewOutputType(IntType),
|
||||
})))
|
||||
}
|
||||
|
||||
func TestPromiseType(t *testing.T) {
|
||||
typ := NewPromiseType(DynamicType)
|
||||
|
||||
// Test that creating an promise type with the same element type does not create a new type.
|
||||
typ2 := NewPromiseType(typ.ElementType)
|
||||
assert.EqualValues(t, typ, typ2)
|
||||
|
||||
// Test that creating an promise type with an element type that is also a promise does not create a new type.
|
||||
typ2 = NewPromiseType(typ)
|
||||
assert.Equal(t, typ, typ2)
|
||||
|
||||
// Test that a promise type is assignable to and from itself.
|
||||
assert.True(t, typ.AssignableFrom(typ))
|
||||
|
||||
// Test that a promise type is assignable from its element type.
|
||||
assert.True(t, NewPromiseType(StringType).AssignableFrom(StringType))
|
||||
|
||||
// Test that promise(T) is assignable from U, where U is assignable to T.
|
||||
assert.True(t, typ.AssignableFrom(BoolType))
|
||||
|
||||
// Test that promise(T) is assignable from promise(U), where U is assignable to T.
|
||||
assert.True(t, typ.AssignableFrom(NewPromiseType(BoolType)))
|
||||
|
||||
// Test that promise(T) is _not_ assignable from U, where U is not assignable to T.
|
||||
assert.False(t, NewPromiseType(BoolType).AssignableFrom(IntType))
|
||||
|
||||
// Test that promise(T) is _not_ assignable from promise(U), where U is not assignable to T.
|
||||
assert.False(t, NewPromiseType(BoolType).AssignableFrom(NewPromiseType(IntType)))
|
||||
|
||||
// Test that traversing an promise(T) returns an promise(U), where U is the result of the inner traversal.
|
||||
typ = NewPromiseType(NewMapType(StringType))
|
||||
testTraverse(t, typ, hcl.TraverseAttr{Name: "foo"}, NewPromiseType(StringType), false)
|
||||
testTraverse(t, typ, hcl.TraverseIndex{Key: cty.StringVal("foo")}, NewPromiseType(StringType), false)
|
||||
testTraverse(t, typ, hcl.TraverseIndex{Key: cty.NumberIntVal(0)}, NewPromiseType(StringType), false)
|
||||
testTraverse(t, typ, hcl.TraverseIndex{Key: encapsulateType(IntType)}, NewPromiseType(StringType), false)
|
||||
|
||||
// Test that ResolvePromises correctly handles nested promises.
|
||||
assert.Equal(t, NewOptionalType(BoolType), ResolvePromises(NewOptionalType(NewPromiseType(BoolType))))
|
||||
assert.Equal(t, BoolType, ResolvePromises(NewPromiseType(BoolType)))
|
||||
assert.Equal(t, BoolType, ResolvePromises(NewPromiseType(BoolType)))
|
||||
assert.Equal(t, NewMapType(BoolType), ResolvePromises(NewMapType(NewPromiseType(BoolType))))
|
||||
assert.Equal(t, NewListType(BoolType), ResolvePromises(NewListType(NewPromiseType(BoolType))))
|
||||
assert.Equal(t, NewUnionType(BoolType, IntType),
|
||||
ResolvePromises(NewUnionType(NewPromiseType(BoolType), NewPromiseType(IntType))))
|
||||
assert.Equal(t, NewObjectType(map[string]Type{
|
||||
"bool": BoolType,
|
||||
"int": IntType,
|
||||
}), ResolvePromises(NewObjectType(map[string]Type{
|
||||
"bool": NewPromiseType(BoolType),
|
||||
"int": NewPromiseType(IntType),
|
||||
})))
|
||||
|
||||
// Test that NewPromiseType correctly handles nested promises.
|
||||
assert.Equal(t, NewPromiseType(NewOptionalType(BoolType)), NewPromiseType(NewOptionalType(NewPromiseType(BoolType))))
|
||||
assert.Equal(t, NewPromiseType(BoolType), NewPromiseType(NewPromiseType(BoolType)))
|
||||
assert.Equal(t, NewPromiseType(BoolType), NewPromiseType(NewPromiseType(BoolType)))
|
||||
assert.Equal(t, NewPromiseType(NewMapType(BoolType)), NewPromiseType(NewMapType(NewPromiseType(BoolType))))
|
||||
assert.Equal(t, NewPromiseType(NewListType(BoolType)), NewPromiseType(NewListType(NewPromiseType(BoolType))))
|
||||
assert.Equal(t, NewPromiseType(NewUnionType(BoolType, IntType)),
|
||||
NewPromiseType(NewUnionType(NewPromiseType(BoolType), NewPromiseType(IntType))))
|
||||
assert.Equal(t, NewPromiseType(NewObjectType(map[string]Type{
|
||||
"bool": BoolType,
|
||||
"int": IntType,
|
||||
})), NewPromiseType(NewObjectType(map[string]Type{
|
||||
"bool": NewPromiseType(BoolType),
|
||||
"int": NewPromiseType(IntType),
|
||||
})))
|
||||
}
|
||||
|
||||
func TestMapType(t *testing.T) {
|
||||
typ := NewMapType(DynamicType)
|
||||
|
||||
// Test that creating an map type with the same element type does not create a new type.
|
||||
typ2 := NewMapType(typ.ElementType)
|
||||
assert.EqualValues(t, typ, typ2)
|
||||
|
||||
// Test that a map type is assignable to and from itself.
|
||||
assert.True(t, typ.AssignableFrom(typ))
|
||||
|
||||
// Test that map(T) is _not_ assignable from U, where U is not map(T).
|
||||
assert.False(t, typ.AssignableFrom(BoolType))
|
||||
|
||||
// Test that map(T) is assignable from map(U), where U is assignable to T.
|
||||
assert.True(t, typ.AssignableFrom(NewMapType(BoolType)))
|
||||
|
||||
// Test that map(T) is convertible from object(K_0=U_0, .., K_N=U_N) where unify(U_0, ..., U_N) is assignable to T.
|
||||
assert.True(t, typ.ConversionFrom(NewObjectType(map[string]Type{
|
||||
"foo": IntType,
|
||||
"bar": NumberType,
|
||||
"baz": StringType,
|
||||
})).Exists())
|
||||
|
||||
// Test that map(T) is _not_ assignable from map(U), where U is not assignable to T.
|
||||
assert.False(t, NewMapType(BoolType).AssignableFrom(NewMapType(IntType)))
|
||||
|
||||
// Test that traversing a map(T) with a type that is convertible to string returns T.
|
||||
testTraverse(t, typ, hcl.TraverseAttr{Name: "foo"}, typ.ElementType, false)
|
||||
testTraverse(t, typ, hcl.TraverseIndex{Key: cty.StringVal("foo")}, typ.ElementType, false)
|
||||
testTraverse(t, typ, hcl.TraverseIndex{Key: cty.NumberIntVal(0)}, typ.ElementType, false)
|
||||
testTraverse(t, typ, hcl.TraverseIndex{Key: encapsulateType(IntType)}, typ.ElementType, false)
|
||||
|
||||
// Test that traversing a map(T) with a type that is not convertible to string returns DynamicType and an error.
|
||||
testTraverse(t, typ, hcl.TraverseIndex{Key: cty.ListVal([]cty.Value{cty.NumberIntVal(0)})}, typ.ElementType, true)
|
||||
testTraverse(t, typ, hcl.TraverseIndex{Key: encapsulateType(typ)}, typ.ElementType, true)
|
||||
}
|
||||
|
||||
func TestListType(t *testing.T) {
|
||||
typ := NewListType(DynamicType)
|
||||
|
||||
// Test that creating an list type with the same element type does not create a new type.
|
||||
typ2 := NewListType(typ.ElementType)
|
||||
assert.EqualValues(t, typ, typ2)
|
||||
|
||||
// Test that an list type is assignable to and from itself.
|
||||
assert.True(t, typ.AssignableFrom(typ))
|
||||
|
||||
// Test that list(T) is _not_ assignable from U, where U is not list(T).
|
||||
assert.False(t, typ.AssignableFrom(BoolType))
|
||||
|
||||
// Test that list(T) is assignable from list(U), where U is assignable to T.
|
||||
assert.True(t, typ.AssignableFrom(NewListType(BoolType)))
|
||||
|
||||
// Test that list(T) is _not_ assignable from list(U), where U is not assignable to T.
|
||||
assert.False(t, NewListType(BoolType).AssignableFrom(NewListType(IntType)))
|
||||
|
||||
// Test that traversing a list(T) with a type that is convertible to number returns T.
|
||||
testTraverse(t, typ, hcl.TraverseAttr{Name: "0"}, typ.ElementType, false)
|
||||
testTraverse(t, typ, hcl.TraverseIndex{Key: cty.StringVal("0")}, typ.ElementType, false)
|
||||
testTraverse(t, typ, hcl.TraverseIndex{Key: cty.NumberIntVal(0)}, typ.ElementType, false)
|
||||
testTraverse(t, typ, hcl.TraverseIndex{Key: encapsulateType(IntType)}, typ.ElementType, false)
|
||||
|
||||
// Test that traversing a list(T) with a type that is not convertible to number returns DynamicType and an error.
|
||||
testTraverse(t, typ, hcl.TraverseIndex{Key: cty.ListVal([]cty.Value{cty.NumberIntVal(0)})}, typ.ElementType, true)
|
||||
testTraverse(t, typ, hcl.TraverseIndex{Key: encapsulateType(typ)}, typ.ElementType, true)
|
||||
}
|
||||
|
||||
func TestSetType(t *testing.T) {
|
||||
typ := NewSetType(DynamicType)
|
||||
|
||||
// Test that creating an set type with the same element type does not create a new type.
|
||||
typ2 := NewSetType(typ.ElementType)
|
||||
assert.EqualValues(t, typ, typ2)
|
||||
|
||||
// Test that an set type is assignable to and from itself.
|
||||
assert.True(t, typ.AssignableFrom(typ))
|
||||
|
||||
// Test that set(T) is _not_ assignable from U, where U is not set(T).
|
||||
assert.False(t, typ.AssignableFrom(BoolType))
|
||||
|
||||
// Test that set(T) is assignable from set(U), where U is assignable to T.
|
||||
assert.True(t, typ.AssignableFrom(NewSetType(BoolType)))
|
||||
|
||||
// Test that set(T) is _not_ assignable from set(U), where U is not assignable to T.
|
||||
assert.False(t, NewSetType(BoolType).AssignableFrom(NewSetType(IntType)))
|
||||
|
||||
// Test that traversing a set(T) fails.
|
||||
testTraverse(t, typ, hcl.TraverseAttr{Name: "0"}, DynamicType, true)
|
||||
testTraverse(t, typ, hcl.TraverseIndex{Key: cty.StringVal("0")}, DynamicType, true)
|
||||
testTraverse(t, typ, hcl.TraverseIndex{Key: cty.NumberIntVal(0)}, DynamicType, true)
|
||||
testTraverse(t, typ, hcl.TraverseIndex{Key: encapsulateType(IntType)}, DynamicType, true)
|
||||
}
|
||||
|
||||
func TestUnionType(t *testing.T) {
|
||||
typ := NewUnionType(BoolType, IntType, NumberType, StringType).(*UnionType)
|
||||
|
||||
// Test that creating a union with the same element types does not create a new type.
|
||||
typ2 := NewUnionType(BoolType, IntType, NumberType, StringType).(*UnionType)
|
||||
assert.EqualValues(t, typ, typ2)
|
||||
|
||||
// Test that creating a union with duplicated element types unifies all of the duplicated types.
|
||||
assert.Equal(t, BoolType, NewUnionType(BoolType, BoolType))
|
||||
assert.Equal(t, typ, NewUnionType(BoolType, IntType, IntType, NumberType, StringType))
|
||||
|
||||
// Test that a union type is assignable to and from itself.
|
||||
assert.True(t, typ.AssignableFrom(typ))
|
||||
|
||||
// Test that a union type is assignable from each of its element types.
|
||||
for _, et := range typ.ElementTypes {
|
||||
assert.True(t, typ.AssignableFrom(et))
|
||||
}
|
||||
|
||||
// Test that union(T_0, ..., T_N) is assignable from union(U_0, ..., U_M) if union(T_0, ..., T_N) is assignable
|
||||
// from all of U_0 through U_M.
|
||||
assert.True(t, typ.AssignableFrom(NewUnionType(BoolType, IntType)))
|
||||
assert.True(t, NewUnionType(NoneType, StringType).ConversionFrom(typ).Exists())
|
||||
|
||||
// Test that union(T_0, ..., T_N) is _not_ assignable from union(U_0, ..., U_M) if union(T_0, ..., T_N) is not
|
||||
// assignable from any of U_0 through U_M.
|
||||
assert.False(t, typ.AssignableFrom(NewUnionType(BoolType, NewOptionalType(NumberType))))
|
||||
|
||||
// Test that traversing a union type fails if the element type cannot be traversed.
|
||||
testTraverse(t, typ, hcl.TraverseAttr{Name: "foo"}, DynamicType, true)
|
||||
testTraverse(t, typ, hcl.TraverseIndex{Key: cty.StringVal("foo")}, DynamicType, true)
|
||||
testTraverse(t, typ, hcl.TraverseIndex{Key: cty.NumberIntVal(0)}, DynamicType, true)
|
||||
testTraverse(t, typ, hcl.TraverseIndex{Key: encapsulateType(typ)}, DynamicType, true)
|
||||
|
||||
// Test that traversing a union type succeeds if some element type can be traversed.
|
||||
typ = NewUnionType(typ, NewObjectType(map[string]Type{"foo": StringType}), NewListType(StringType)).(*UnionType)
|
||||
testTraverse(t, typ, hcl.TraverseAttr{Name: "foo"}, StringType, false)
|
||||
testTraverse(t, typ, hcl.TraverseIndex{Key: cty.StringVal("foo")}, StringType, false)
|
||||
testTraverse(t, typ, hcl.TraverseIndex{Key: cty.NumberIntVal(0)}, StringType, false)
|
||||
testTraverse(t, typ, hcl.TraverseIndex{Key: encapsulateType(StringType)}, StringType, false)
|
||||
|
||||
// Test that traversing a union type produces a union if more than one element can be traversed.
|
||||
typ = NewUnionType(NewMapType(IntType), NewObjectType(map[string]Type{"foo": StringType})).(*UnionType)
|
||||
testTraverse(t, typ, hcl.TraverseAttr{Name: "foo"}, NewUnionType(StringType, IntType), false)
|
||||
testTraverse(t, typ, hcl.TraverseIndex{Key: cty.StringVal("foo")}, NewUnionType(StringType, IntType), false)
|
||||
testTraverse(t, typ, hcl.TraverseIndex{Key: cty.NumberIntVal(0)}, IntType, false)
|
||||
testTraverse(t, typ, hcl.TraverseIndex{Key: encapsulateType(IntType)}, NewUnionType(StringType, IntType), false)
|
||||
}
|
||||
|
||||
func TestObjectType(t *testing.T) {
|
||||
typ := NewObjectType(map[string]Type{
|
||||
"foo": BoolType,
|
||||
"bar": IntType,
|
||||
"baz": NumberType,
|
||||
"qux": NewOptionalType(BoolType),
|
||||
})
|
||||
|
||||
// Test that creating a union with the same element types does not create a new type.
|
||||
typ2 := NewObjectType(map[string]Type{
|
||||
"foo": BoolType,
|
||||
"bar": IntType,
|
||||
"baz": NumberType,
|
||||
"qux": NewOptionalType(BoolType),
|
||||
})
|
||||
assert.EqualValues(t, typ, typ2)
|
||||
|
||||
// Test that an object type is assignable to and from itself.
|
||||
assert.True(t, typ.AssignableFrom(typ))
|
||||
|
||||
// Test that object(K_0=T_0, ..., K_N=T_N) is assignable from object(K_0=U_0, ..., K_N=U_N) if for each key K_i
|
||||
// T_i is assignable from U_i.
|
||||
assert.True(t, typ.ConversionFrom(NewObjectType(map[string]Type{
|
||||
"foo": BoolType,
|
||||
"bar": IntType,
|
||||
"baz": IntType,
|
||||
"qux": BoolType,
|
||||
})).Exists())
|
||||
// Test that object(K_0=T_0, ..., K_N=T_N) is assignable from object(K_0=U_0, ..., K_M=U_M) if M < N and for each
|
||||
// key K_i where 0 <= i <= M, T_i is assignable from U_i and for each K_j where M < j <= N, T_j is optional.
|
||||
assert.True(t, typ.ConversionFrom(NewObjectType(map[string]Type{
|
||||
"foo": BoolType,
|
||||
"bar": IntType,
|
||||
"baz": NumberType,
|
||||
})).Exists())
|
||||
|
||||
// Test that object(K_0=T_0, ..., K_N=T_N) is _unsafely_ convertible from object(L_0=U_0, ..., L_M=U_M) if there exists
|
||||
// some key K_i a matching key K_i exists and T_i is unsafely convertible from U_i.
|
||||
assert.Equal(t, UnsafeConversion, typ.ConversionFrom(NewObjectType(map[string]Type{
|
||||
"foo": BoolType,
|
||||
"bar": IntType,
|
||||
"baz": NumberType,
|
||||
"qux": StringType,
|
||||
})))
|
||||
assert.Equal(t, UnsafeConversion, typ.ConversionFrom(NewObjectType(map[string]Type{
|
||||
"foo": BoolType,
|
||||
"bar": IntType,
|
||||
"baz": StringType,
|
||||
})))
|
||||
|
||||
// Test that traversing an object type with a property name K_i returns T_i.
|
||||
testTraverse(t, typ, hcl.TraverseAttr{Name: "foo"}, BoolType, false)
|
||||
testTraverse(t, typ, hcl.TraverseAttr{Name: "bar"}, IntType, false)
|
||||
testTraverse(t, typ, hcl.TraverseAttr{Name: "baz"}, NumberType, false)
|
||||
testTraverse(t, typ, hcl.TraverseAttr{Name: "qux"}, NewOptionalType(BoolType), false)
|
||||
testTraverse(t, typ, hcl.TraverseIndex{Key: cty.StringVal("foo")}, BoolType, false)
|
||||
testTraverse(t, typ, hcl.TraverseIndex{Key: cty.StringVal("bar")}, IntType, false)
|
||||
testTraverse(t, typ, hcl.TraverseIndex{Key: cty.StringVal("baz")}, NumberType, false)
|
||||
testTraverse(t, typ, hcl.TraverseIndex{Key: cty.StringVal("qux")}, NewOptionalType(BoolType), false)
|
||||
|
||||
// Test that traversing an object type with a dynamic value produces the union of the object's property types..
|
||||
testTraverse(t, typ, hcl.TraverseIndex{Key: encapsulateType(StringType)}, NewUnionType(BoolType, IntType,
|
||||
NumberType, NewOptionalType(BoolType)), false)
|
||||
|
||||
// Test that traversing an object type with any other type fails.
|
||||
testTraverse(t, typ, hcl.TraverseIndex{Key: cty.NumberIntVal(0)}, DynamicType, true)
|
||||
testTraverse(t, typ, hcl.TraverseIndex{Key: encapsulateType(typ)}, DynamicType, true)
|
||||
}
|
||||
|
||||
func TestOpaqueType(t *testing.T) {
|
||||
foo, err := NewOpaqueType("foo")
|
||||
assert.NotNil(t, foo)
|
||||
assert.NoError(t, err)
|
||||
|
||||
foo2, ok := GetOpaqueType("foo")
|
||||
assert.EqualValues(t, foo, foo2)
|
||||
assert.True(t, ok)
|
||||
|
||||
foo3, err := NewOpaqueType("foo")
|
||||
assert.Nil(t, foo3)
|
||||
assert.Error(t, err)
|
||||
|
||||
bar, ok := GetOpaqueType("bar")
|
||||
assert.Nil(t, bar)
|
||||
assert.False(t, ok)
|
||||
|
||||
bar, err = NewOpaqueType("bar")
|
||||
assert.NotNil(t, bar)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.NotEqual(t, foo, bar)
|
||||
}
|
||||
|
||||
func TestInputType(t *testing.T) {
|
||||
// Test that InputType(DynamicType) just returns DynamicType.
|
||||
assert.Equal(t, DynamicType, InputType(DynamicType))
|
||||
|
||||
// Test that InputType(T) correctly recurses through constructed types. The result of InputType(T) should be
|
||||
// union(innerInputType(T), output(innerInputType(T))), where innerInputType(T) recurses thorough constructed
|
||||
// types.
|
||||
|
||||
assert.Equal(t, NewUnionType(BoolType, NewOutputType(BoolType)), InputType(BoolType))
|
||||
|
||||
assert.Equal(t, NewUnionType(
|
||||
NewOptionalType(NewUnionType(BoolType, NewOutputType(BoolType))),
|
||||
NewOutputType(NewOptionalType(BoolType))), InputType(NewOptionalType(BoolType)))
|
||||
|
||||
assert.Equal(t, NewUnionType(
|
||||
NewPromiseType(NewUnionType(BoolType, NewOutputType(BoolType))),
|
||||
NewOutputType(BoolType)), InputType(NewPromiseType(BoolType)))
|
||||
|
||||
assert.Equal(t, NewUnionType(
|
||||
NewMapType(NewUnionType(BoolType, NewOutputType(BoolType))),
|
||||
NewOutputType(NewMapType(BoolType))), InputType(NewMapType(BoolType)))
|
||||
|
||||
assert.Equal(t, NewUnionType(
|
||||
NewListType(NewUnionType(BoolType, NewOutputType(BoolType))),
|
||||
NewOutputType(NewListType(BoolType))), InputType(NewListType(BoolType)))
|
||||
|
||||
assert.Equal(t, NewUnionType(
|
||||
NewUnionType(BoolType, IntType, NewOutputType(BoolType), NewOutputType(IntType)),
|
||||
NewOutputType(NewUnionType(BoolType, IntType))),
|
||||
InputType(NewUnionType(BoolType, IntType)))
|
||||
|
||||
assert.Equal(t, NewUnionType(
|
||||
NewObjectType(map[string]Type{"foo": NewUnionType(BoolType, NewOutputType(BoolType))}),
|
||||
NewOutputType(NewObjectType(map[string]Type{"foo": BoolType}))),
|
||||
InputType(NewObjectType(map[string]Type{"foo": BoolType})))
|
||||
|
||||
assert.True(t, InputType(BoolType).ConversionFrom(BoolType).Exists())
|
||||
assert.True(t, InputType(NumberType).ConversionFrom(NumberType).Exists())
|
||||
}
|
||||
|
||||
func assertUnified(t *testing.T, expectedSafe, expectedUnsafe Type, types ...Type) {
|
||||
actualSafe, actualUnsafe := UnifyTypes(types...)
|
||||
assert.Equal(t, expectedSafe, actualSafe)
|
||||
assert.Equal(t, expectedUnsafe, actualUnsafe)
|
||||
|
||||
// Reverse the types and ensure we get the same results.
|
||||
for i, j := 0, len(types)-1; i < j; i, j = i+1, j-1 {
|
||||
types[i], types[j] = types[j], types[i]
|
||||
}
|
||||
actualSafe2, actualUnsafe2 := UnifyTypes(types...)
|
||||
assert.Equal(t, actualSafe, actualSafe2)
|
||||
assert.Equal(t, actualUnsafe, actualUnsafe2)
|
||||
}
|
||||
|
||||
func TestUnifyType(t *testing.T) {
|
||||
// Number, int, and bool unify with string by preferring string.
|
||||
assertUnified(t, StringType, StringType, NumberType, StringType)
|
||||
assertUnified(t, StringType, StringType, IntType, StringType)
|
||||
assertUnified(t, StringType, StringType, BoolType, StringType)
|
||||
|
||||
// Number and int unify by preferring number.
|
||||
assertUnified(t, NumberType, NumberType, IntType, NumberType)
|
||||
|
||||
// Number or int and bool unify by preferring number or int.
|
||||
assertUnified(t, NewUnionType(NumberType, BoolType), NumberType, BoolType, NumberType)
|
||||
assertUnified(t, NewUnionType(IntType, BoolType), IntType, BoolType, IntType)
|
||||
|
||||
// Two collection types of the same kind unify according to the unification of their element types.
|
||||
assertUnified(t, NewMapType(StringType), NewMapType(StringType), NewMapType(BoolType), NewMapType(StringType))
|
||||
assertUnified(t, NewListType(StringType), NewListType(StringType), NewListType(BoolType), NewListType(StringType))
|
||||
assertUnified(t, NewSetType(StringType), NewSetType(StringType), NewSetType(BoolType), NewSetType(StringType))
|
||||
|
||||
// List and set types unify by preferring the list type.
|
||||
assertUnified(t, NewListType(StringType), NewListType(StringType), NewListType(StringType), NewSetType(BoolType))
|
||||
assertUnified(t, NewListType(StringType), NewListType(StringType), NewListType(BoolType), NewSetType(StringType))
|
||||
|
||||
assert.True(t, StringType.ConversionFrom(NewOptionalType(NewUnionType(NewMapType(StringType), BoolType))).Exists())
|
||||
|
||||
// Map and object types unify by preferring the map type.
|
||||
m0, m1 := NewObjectType(map[string]Type{"foo": StringType}), NewObjectType(map[string]Type{"foo": BoolType})
|
||||
assertUnified(t, NewMapType(StringType), NewMapType(StringType), m0, NewMapType(BoolType))
|
||||
assertUnified(t, NewMapType(StringType), NewMapType(StringType), m1, NewMapType(StringType))
|
||||
|
||||
// List or set and tuple types unify by preferring the list or set type.
|
||||
t0, t1 := NewTupleType(NumberType, BoolType), NewTupleType(StringType, NumberType)
|
||||
assertUnified(t, NewListType(StringType), NewListType(StringType), t0, NewListType(StringType))
|
||||
assertUnified(t, NewListType(StringType), NewListType(StringType), t1, NewListType(BoolType))
|
||||
assertUnified(t, NewUnionType(t0, NewSetType(StringType)), NewSetType(StringType), t0, NewSetType(StringType))
|
||||
assertUnified(t, NewUnionType(t1, NewSetType(BoolType)), NewSetType(StringType), t1, NewSetType(BoolType))
|
||||
|
||||
// The dynamic type unifies with any other type by selecting the other type.
|
||||
assertUnified(t, NewUnionType(BoolType, DynamicType), BoolType, BoolType, DynamicType)
|
||||
|
||||
// Object types unify by constructing a new object type whose attributes are the unification of the two input types.
|
||||
m2 := NewObjectType(map[string]Type{"bar": StringType})
|
||||
m3 := NewObjectType(map[string]Type{"foo": NewOptionalType(StringType), "bar": NewOptionalType(StringType)})
|
||||
m4 := NewObjectType(map[string]Type{"foo": NewMapType(StringType), "bar": NewListType(StringType)})
|
||||
m5 := NewObjectType(map[string]Type{
|
||||
"foo": NewOptionalType(NewUnionType(NewMapType(StringType), StringType, NoneType)),
|
||||
"bar": NewOptionalType(NewUnionType(NewListType(StringType), StringType, NoneType)),
|
||||
})
|
||||
assertUnified(t, m0, m0, m0, m1)
|
||||
assertUnified(t, m3, m3, m0, m2)
|
||||
assertUnified(t, m5, m5, m4, m2, m0, m1)
|
||||
assertUnified(t, m5, m5, m4, m0, m2, m1)
|
||||
|
||||
// Tuple types unify by constructing a new tuple type whose element types are the unification of the corresponding
|
||||
// element types.
|
||||
t2 := NewTupleType(StringType, NumberType)
|
||||
t3 := NewTupleType(StringType, IntType)
|
||||
t4 := NewTupleType(NumberType, BoolType, StringType)
|
||||
t5 := NewTupleType(NumberType, BoolType, NewOptionalType(StringType))
|
||||
assertUnified(t, NewUnionType(t0, t1), t2, t0, t1)
|
||||
assertUnified(t, t2, t2, t3, t1)
|
||||
assertUnified(t, t5, t5, t4, t0)
|
||||
|
||||
//
|
||||
// assertUnified(t, NewUnionType(BoolType, IntType), IntType, BoolType, IntType)
|
||||
// assertUnified(t, NewOptionalType(NumberType), NewOptionalType(NumberType), IntType, NewOptionalType(NumberType))
|
||||
// assertUnified(t, NewOptionalType(BoolType), NewOptionalType(BoolType), BoolType, NewOptionalType(BoolType))
|
||||
// assertUnified(t, NewOutputType(BoolType), NewOutputType(BoolType), BoolType, NewOutputType(BoolType))
|
||||
// assertUnified(t, NewPromiseType(BoolType), NewPromiseType(BoolType), BoolType, NewPromiseType(BoolType))
|
||||
// assertUnified(t, AnyType, AnyType, BoolType, IntType, AnyType)
|
||||
//
|
||||
// assertUnified(t, BoolType, BoolType, DynamicType, BoolType)
|
||||
|
||||
// t0 := Type(NewObjectType(map[string]Type{"foo": IntType}))
|
||||
// t1 := Type(NewObjectType(map[string]Type{"foo": IntType, "bar": NewOptionalType(NumberType)}))
|
||||
//
|
||||
// assert.Equal(t, NewMapType(AnyType), unifyTypes(NewMapType(AnyType), t0))
|
||||
// assert.Equal(t, t1, unifyTypes(t0, t1))
|
||||
// assert.Equal(t, t1, unifyTypes(t1, t0))
|
||||
//
|
||||
// t0 = NewOutputType(NumberType)
|
||||
// t1 = NewOutputType(NewUnionType(NumberType, IntType))
|
||||
// assert.Equal(t, t0, unifyTypes(t0, t1))
|
||||
// assert.Equal(t, t0, unifyTypes(t1, t0))
|
||||
}
|
242
pkg/codegen/hcl2/model/type_tuple.go
Normal file
242
pkg/codegen/hcl2/model/type_tuple.go
Normal file
|
@ -0,0 +1,242 @@
|
|||
// 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 model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||
"github.com/pulumi/pulumi/pkg/codegen/hcl2/syntax"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
// TupleType represents values that are a sequence of independently-typed elements.
|
||||
type TupleType struct {
|
||||
// ElementTypes are the types of the tuple's elements.
|
||||
ElementTypes []Type
|
||||
|
||||
elementUnion Type
|
||||
s string
|
||||
}
|
||||
|
||||
// The set of tuple types, indexed by string representation.
|
||||
var tupleTypes = map[string]*TupleType{}
|
||||
|
||||
// NewTupleType creates a new tuple type with the given element types.
|
||||
func NewTupleType(elementTypes ...Type) Type {
|
||||
t := &TupleType{ElementTypes: elementTypes}
|
||||
if t, ok := tupleTypes[t.String()]; ok {
|
||||
return t
|
||||
}
|
||||
tupleTypes[t.String()] = t
|
||||
return t
|
||||
}
|
||||
|
||||
// SyntaxNode returns the syntax node for the type. This is always syntax.None.
|
||||
func (*TupleType) SyntaxNode() hclsyntax.Node {
|
||||
return syntax.None
|
||||
}
|
||||
|
||||
// Traverse attempts to traverse the tuple type with the given traverser. This always fails.
|
||||
func (t *TupleType) Traverse(traverser hcl.Traverser) (Traversable, hcl.Diagnostics) {
|
||||
key, keyType := GetTraverserKey(traverser)
|
||||
|
||||
if !InputType(NumberType).AssignableFrom(keyType) {
|
||||
return DynamicType, hcl.Diagnostics{unsupportedTupleIndex(traverser.SourceRange())}
|
||||
}
|
||||
|
||||
if key == cty.DynamicVal {
|
||||
if t.elementUnion == nil {
|
||||
t.elementUnion = NewUnionType(t.ElementTypes...)
|
||||
}
|
||||
return t.elementUnion, nil
|
||||
}
|
||||
|
||||
elementIndex, acc := key.AsBigFloat().Int64()
|
||||
if acc != big.Exact {
|
||||
return DynamicType, hcl.Diagnostics{unsupportedTupleIndex(traverser.SourceRange())}
|
||||
}
|
||||
if elementIndex < 0 || elementIndex > int64(len(t.ElementTypes)) {
|
||||
return DynamicType, hcl.Diagnostics{tupleIndexOutOfRange(len(t.ElementTypes), traverser.SourceRange())}
|
||||
}
|
||||
return t.ElementTypes[int(elementIndex)], nil
|
||||
}
|
||||
|
||||
// AssignableFrom returns true if this type is assignable from the indicated source type..
|
||||
func (t *TupleType) AssignableFrom(src Type) bool {
|
||||
return assignableFrom(t, src, func() bool {
|
||||
if src, ok := src.(*TupleType); ok {
|
||||
for i := 0; i < len(t.ElementTypes); i++ {
|
||||
srcElement := NoneType
|
||||
if i < len(src.ElementTypes) {
|
||||
srcElement = src.ElementTypes[i]
|
||||
}
|
||||
if !t.ElementTypes[i].AssignableFrom(srcElement) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
type tupleElementUnifier struct {
|
||||
elementTypes []Type
|
||||
any bool
|
||||
conversionKind ConversionKind
|
||||
}
|
||||
|
||||
func (u *tupleElementUnifier) unify(t *TupleType) {
|
||||
if !u.any {
|
||||
u.elementTypes, u.any, u.conversionKind = append([]Type(nil), t.ElementTypes...), true, SafeConversion
|
||||
} else {
|
||||
min := len(u.elementTypes)
|
||||
if l := len(t.ElementTypes); l < min {
|
||||
min = l
|
||||
}
|
||||
|
||||
for i := 0; i < min; i++ {
|
||||
element, ck := u.elementTypes[i].unify(t.ElementTypes[i])
|
||||
if ck < u.conversionKind {
|
||||
u.conversionKind = ck
|
||||
}
|
||||
u.elementTypes[i] = element
|
||||
}
|
||||
|
||||
if len(u.elementTypes) > len(t.ElementTypes) {
|
||||
for i := min; i < len(u.elementTypes); i++ {
|
||||
u.elementTypes[i] = NewOptionalType(u.elementTypes[i])
|
||||
}
|
||||
} else {
|
||||
for _, t := range t.ElementTypes[min:] {
|
||||
u.elementTypes = append(u.elementTypes, NewOptionalType(t))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TupleType) ConversionFrom(src Type) ConversionKind {
|
||||
return t.conversionFrom(src, false)
|
||||
}
|
||||
|
||||
func (t *TupleType) conversionFrom(src Type, unifying bool) ConversionKind {
|
||||
return conversionFrom(t, src, unifying, func() ConversionKind {
|
||||
switch src := src.(type) {
|
||||
case *TupleType:
|
||||
// When unifying, we will unify two tuples of different length to a new tuple, where elements with matching
|
||||
// indices are unified and elements that are missing are treated as having type None.
|
||||
if unifying {
|
||||
var unifier tupleElementUnifier
|
||||
unifier.unify(t)
|
||||
unifier.unify(src)
|
||||
return unifier.conversionKind
|
||||
}
|
||||
|
||||
if len(t.ElementTypes) != len(src.ElementTypes) {
|
||||
return NoConversion
|
||||
}
|
||||
|
||||
conversionKind := SafeConversion
|
||||
for i, dst := range t.ElementTypes {
|
||||
if ck := dst.conversionFrom(src.ElementTypes[i], unifying); ck < conversionKind {
|
||||
conversionKind = ck
|
||||
}
|
||||
}
|
||||
|
||||
// When unifying, the conversion kind of two tuple types is the lesser of the conversion in each direction.
|
||||
if unifying {
|
||||
conversionTo := src.conversionFrom(t, false)
|
||||
if conversionTo < conversionKind {
|
||||
conversionKind = conversionTo
|
||||
}
|
||||
}
|
||||
|
||||
return conversionKind
|
||||
case *ListType:
|
||||
conversionKind := UnsafeConversion
|
||||
for _, t := range t.ElementTypes {
|
||||
if ck := t.conversionFrom(src.ElementType, unifying); ck < conversionKind {
|
||||
conversionKind = ck
|
||||
}
|
||||
}
|
||||
return conversionKind
|
||||
case *SetType:
|
||||
conversionKind := UnsafeConversion
|
||||
for _, t := range t.ElementTypes {
|
||||
if ck := t.conversionFrom(src.ElementType, unifying); ck < conversionKind {
|
||||
conversionKind = ck
|
||||
}
|
||||
}
|
||||
return conversionKind
|
||||
}
|
||||
return NoConversion
|
||||
})
|
||||
}
|
||||
|
||||
func (t *TupleType) String() string {
|
||||
if t.s == "" {
|
||||
elements := make([]string, len(t.ElementTypes))
|
||||
for i, e := range t.ElementTypes {
|
||||
elements[i] = e.String()
|
||||
}
|
||||
t.s = fmt.Sprintf("tuple(%s)", strings.Join(elements, ", "))
|
||||
}
|
||||
return t.s
|
||||
}
|
||||
|
||||
func (t *TupleType) unify(other Type) (Type, ConversionKind) {
|
||||
return unify(t, other, func() (Type, ConversionKind) {
|
||||
switch other := other.(type) {
|
||||
case *TupleType:
|
||||
// When unifying, we will unify two tuples of different length to a new tuple, where elements with matching
|
||||
// indices are unified and elements that are missing are treated as having type None.
|
||||
var unifier tupleElementUnifier
|
||||
unifier.unify(t)
|
||||
unifier.unify(other)
|
||||
return NewTupleType(unifier.elementTypes...), unifier.conversionKind
|
||||
case *ListType:
|
||||
// Prefer the list type, but unify the element type.
|
||||
elementType, conversionKind := other.ElementType, SafeConversion
|
||||
for _, t := range t.ElementTypes {
|
||||
element, ck := elementType.unify(t)
|
||||
if ck < conversionKind {
|
||||
conversionKind = ck
|
||||
}
|
||||
elementType = element
|
||||
}
|
||||
return NewListType(elementType), conversionKind
|
||||
case *SetType:
|
||||
// Prefer the set type, but unify the element type.
|
||||
elementType, conversionKind := other.ElementType, UnsafeConversion
|
||||
for _, t := range t.ElementTypes {
|
||||
element, ck := elementType.unify(t)
|
||||
if ck < conversionKind {
|
||||
conversionKind = ck
|
||||
}
|
||||
elementType = element
|
||||
}
|
||||
return NewSetType(elementType), conversionKind
|
||||
default:
|
||||
// Otherwise, prefer the tuple type.
|
||||
return t, t.conversionFrom(other, true)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (*TupleType) isType() {}
|
222
pkg/codegen/hcl2/model/type_union.go
Normal file
222
pkg/codegen/hcl2/model/type_union.go
Normal file
|
@ -0,0 +1,222 @@
|
|||
// 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 model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||
"github.com/pulumi/pulumi/pkg/codegen/hcl2/syntax"
|
||||
)
|
||||
|
||||
// UnionType represents values that may be any one of a specified set of types.
|
||||
type UnionType struct {
|
||||
// ElementTypes are the allowable types for the union type.
|
||||
ElementTypes []Type
|
||||
|
||||
s string
|
||||
}
|
||||
|
||||
// The set of union types, indexed by string representation.
|
||||
var unionTypes = map[string]*UnionType{}
|
||||
|
||||
// NewUnionType creates a new union type with the given element types. Any element types that are union types are
|
||||
// replaced with their element types.
|
||||
func NewUnionType(types ...Type) Type {
|
||||
var elementTypes []Type
|
||||
for _, t := range types {
|
||||
if union, isUnion := t.(*UnionType); isUnion {
|
||||
elementTypes = append(elementTypes, union.ElementTypes...)
|
||||
} else {
|
||||
elementTypes = append(elementTypes, t)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(elementTypes, func(i, j int) bool {
|
||||
return elementTypes[i].String() < elementTypes[j].String()
|
||||
})
|
||||
|
||||
dst := 0
|
||||
for src := 0; src < len(elementTypes); {
|
||||
for src < len(elementTypes) && elementTypes[src] == elementTypes[dst] {
|
||||
src++
|
||||
}
|
||||
dst++
|
||||
|
||||
if src < len(elementTypes) {
|
||||
elementTypes[dst] = elementTypes[src]
|
||||
}
|
||||
}
|
||||
elementTypes = elementTypes[:dst]
|
||||
|
||||
if len(elementTypes) == 1 {
|
||||
return elementTypes[0]
|
||||
}
|
||||
|
||||
t := &UnionType{ElementTypes: elementTypes}
|
||||
if t, ok := unionTypes[t.String()]; ok {
|
||||
return t
|
||||
}
|
||||
unionTypes[t.String()] = t
|
||||
return t
|
||||
}
|
||||
|
||||
// NewOptionalType returns a new union(T, None).
|
||||
func NewOptionalType(t Type) Type {
|
||||
return NewUnionType(t, NoneType)
|
||||
}
|
||||
|
||||
// IsOptionalType returns true if t is an optional type.
|
||||
func IsOptionalType(t Type) bool {
|
||||
return t.AssignableFrom(NoneType)
|
||||
}
|
||||
|
||||
// SyntaxNode returns the syntax node for the type. This is always syntax.None.
|
||||
func (*UnionType) SyntaxNode() hclsyntax.Node {
|
||||
return syntax.None
|
||||
}
|
||||
|
||||
// Traverse attempts to traverse the union type with the given traverser. This always fails.
|
||||
func (t *UnionType) Traverse(traverser hcl.Traverser) (Traversable, hcl.Diagnostics) {
|
||||
var types []Type
|
||||
for _, t := range t.ElementTypes {
|
||||
// We handle 'none' specially here: so that traversing an optional type returns an optional type.
|
||||
if t == NoneType {
|
||||
types = append(types, NoneType)
|
||||
} else {
|
||||
// Note that we intentionally drop errors here and assume that the traversal will dynamically succeed.
|
||||
et, diags := t.Traverse(traverser)
|
||||
if !diags.HasErrors() {
|
||||
types = append(types, et.(Type))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch len(types) {
|
||||
case 0:
|
||||
return DynamicType, hcl.Diagnostics{unsupportedReceiverType(t, traverser.SourceRange())}
|
||||
case 1:
|
||||
if types[0] == NoneType {
|
||||
return DynamicType, hcl.Diagnostics{unsupportedReceiverType(t, traverser.SourceRange())}
|
||||
}
|
||||
return types[0], nil
|
||||
default:
|
||||
return NewUnionType(types...), nil
|
||||
}
|
||||
}
|
||||
|
||||
// AssignableFrom returns true if this type is assignable from the indicated source type. A union(T_0, ..., T_N)
|
||||
// from values of type union(U_0, ..., U_M) where all of U_0 through U_M are assignable to some type in
|
||||
// (T_0, ..., T_N) and V where V is assignable to at least one of (T_0, ..., T_N).
|
||||
func (t *UnionType) AssignableFrom(src Type) bool {
|
||||
return assignableFrom(t, src, func() bool {
|
||||
for _, t := range t.ElementTypes {
|
||||
if t.AssignableFrom(src) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
func (t *UnionType) assignableTo(dest Type) bool {
|
||||
for _, t := range t.ElementTypes {
|
||||
if !dest.AssignableFrom(t) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (t *UnionType) ConversionFrom(src Type) ConversionKind {
|
||||
return t.conversionFrom(src, false)
|
||||
}
|
||||
|
||||
func (t *UnionType) conversionFrom(src Type, unifying bool) ConversionKind {
|
||||
return conversionFrom(t, src, unifying, func() ConversionKind {
|
||||
var conversionKind ConversionKind
|
||||
for _, t := range t.ElementTypes {
|
||||
if ck := t.conversionFrom(src, unifying); ck > conversionKind {
|
||||
conversionKind = ck
|
||||
}
|
||||
}
|
||||
return conversionKind
|
||||
})
|
||||
}
|
||||
|
||||
// If all conversions to a dest type from a union type are safe, the conversion is safe.
|
||||
// If no conversions to a dest type from a union type exist, the conversion does not exist.
|
||||
// Otherwise, the conversion is unsafe.
|
||||
func (t *UnionType) conversionTo(dest Type, unifying bool) ConversionKind {
|
||||
conversionKind, exists := SafeConversion, false
|
||||
for _, t := range t.ElementTypes {
|
||||
switch dest.conversionFrom(t, unifying) {
|
||||
case SafeConversion:
|
||||
exists = true
|
||||
case UnsafeConversion:
|
||||
conversionKind, exists = UnsafeConversion, true
|
||||
case NoConversion:
|
||||
conversionKind = UnsafeConversion
|
||||
}
|
||||
}
|
||||
if !exists {
|
||||
return NoConversion
|
||||
}
|
||||
return conversionKind
|
||||
}
|
||||
|
||||
func (t *UnionType) String() string {
|
||||
if t.s == "" {
|
||||
elements := make([]string, len(t.ElementTypes))
|
||||
for i, e := range t.ElementTypes {
|
||||
elements[i] = e.String()
|
||||
}
|
||||
t.s = fmt.Sprintf("union(%s)", strings.Join(elements, ", "))
|
||||
}
|
||||
return t.s
|
||||
}
|
||||
|
||||
func (t *UnionType) unify(other Type) (Type, ConversionKind) {
|
||||
return unify(t, other, func() (Type, ConversionKind) {
|
||||
return t.unifyTo(other)
|
||||
})
|
||||
}
|
||||
|
||||
func (t *UnionType) unifyTo(other Type) (Type, ConversionKind) {
|
||||
switch other := other.(type) {
|
||||
case *UnionType:
|
||||
// If the other type is also a union type, produce a new type that is the union of their elements.
|
||||
elements := make([]Type, 0, len(t.ElementTypes)+len(other.ElementTypes))
|
||||
elements = append(elements, t.ElementTypes...)
|
||||
elements = append(elements, other.ElementTypes...)
|
||||
return NewUnionType(elements...), SafeConversion
|
||||
default:
|
||||
// Otherwise, unify the other type with each element of the union and return a new union type.
|
||||
elements, conversionKind := make([]Type, len(t.ElementTypes)), SafeConversion
|
||||
for i, t := range t.ElementTypes {
|
||||
element, ck := t.unify(other)
|
||||
if ck < conversionKind {
|
||||
conversionKind = ck
|
||||
}
|
||||
elements[i] = element
|
||||
}
|
||||
return NewUnionType(elements...), conversionKind
|
||||
}
|
||||
}
|
||||
|
||||
func (*UnionType) isType() {}
|
52
pkg/codegen/hcl2/model/utilities.go
Normal file
52
pkg/codegen/hcl2/model/utilities.go
Normal file
|
@ -0,0 +1,52 @@
|
|||
// 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 model
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||
|
||||
"github.com/pulumi/pulumi/pkg/codegen/hcl2/syntax"
|
||||
)
|
||||
|
||||
func syntaxOrNone(node hclsyntax.Node) hclsyntax.Node {
|
||||
if node == nil {
|
||||
return syntax.None
|
||||
}
|
||||
return node
|
||||
}
|
||||
|
||||
// SourceOrderLess returns true if the first range precedes the second when ordered by source position. Positions are
|
||||
// ordered first by filename, then by byte offset.
|
||||
func SourceOrderLess(a, b hcl.Range) bool {
|
||||
return a.Filename < b.Filename || a.Start.Byte < b.Start.Byte
|
||||
}
|
||||
|
||||
// SourceOrderBody sorts the contents of an HCL2 body in source order.
|
||||
func SourceOrderBody(body *hclsyntax.Body) []hclsyntax.Node {
|
||||
items := make([]hclsyntax.Node, 0, len(body.Attributes)+len(body.Blocks))
|
||||
for _, attr := range body.Attributes {
|
||||
items = append(items, attr)
|
||||
}
|
||||
for _, block := range body.Blocks {
|
||||
items = append(items, block)
|
||||
}
|
||||
sort.Slice(items, func(i, j int) bool {
|
||||
return SourceOrderLess(items[i].Range(), items[j].Range())
|
||||
})
|
||||
return items
|
||||
}
|
292
pkg/codegen/hcl2/model/visitor.go
Normal file
292
pkg/codegen/hcl2/model/visitor.go
Normal file
|
@ -0,0 +1,292 @@
|
|||
// 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 model
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/pulumi/pulumi/sdk/go/common/util/contract"
|
||||
)
|
||||
|
||||
// An ExpressionVisitor is a function that visits and optionally replaces a node in an expression tree.
|
||||
type ExpressionVisitor func(n Expression) (Expression, hcl.Diagnostics)
|
||||
|
||||
// IdentityVisitor is a ExpressionVisitor that returns the input node unchanged.
|
||||
func IdentityVisitor(n Expression) (Expression, hcl.Diagnostics) {
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func visitAnonymousFunction(n *AnonymousFunctionExpression, pre, post ExpressionVisitor) (Expression, hcl.Diagnostics) {
|
||||
var diagnostics hcl.Diagnostics
|
||||
|
||||
body, diags := VisitExpression(n.Body, pre, post)
|
||||
diagnostics = append(diagnostics, diags...)
|
||||
|
||||
n.Body = body
|
||||
|
||||
expr, diags := post(n)
|
||||
return expr, append(diagnostics, diags...)
|
||||
}
|
||||
|
||||
func visitBinaryOp(n *BinaryOpExpression, pre, post ExpressionVisitor) (Expression, hcl.Diagnostics) {
|
||||
var diagnostics hcl.Diagnostics
|
||||
|
||||
left, diags := VisitExpression(n.LeftOperand, pre, post)
|
||||
diagnostics = append(diagnostics, diags...)
|
||||
|
||||
right, diags := VisitExpression(n.RightOperand, pre, post)
|
||||
diagnostics = append(diagnostics, diags...)
|
||||
|
||||
n.LeftOperand, n.RightOperand = left, right
|
||||
|
||||
expr, diags := post(n)
|
||||
return expr, append(diagnostics, diags...)
|
||||
}
|
||||
|
||||
func visitConditional(n *ConditionalExpression, pre, post ExpressionVisitor) (Expression, hcl.Diagnostics) {
|
||||
var diagnostics hcl.Diagnostics
|
||||
|
||||
condition, diags := VisitExpression(n.Condition, pre, post)
|
||||
diagnostics = append(diagnostics, diags...)
|
||||
|
||||
trueResult, diags := VisitExpression(n.TrueResult, pre, post)
|
||||
diagnostics = append(diagnostics, diags...)
|
||||
|
||||
falseResult, diags := VisitExpression(n.FalseResult, pre, post)
|
||||
diagnostics = append(diagnostics, diags...)
|
||||
|
||||
n.Condition, n.TrueResult, n.FalseResult = condition, trueResult, falseResult
|
||||
|
||||
expr, diags := post(n)
|
||||
return expr, append(diagnostics, diags...)
|
||||
}
|
||||
|
||||
func visitFor(n *ForExpression, pre, post ExpressionVisitor) (Expression, hcl.Diagnostics) {
|
||||
var diagnostics hcl.Diagnostics
|
||||
|
||||
collection, diags := VisitExpression(n.Collection, pre, post)
|
||||
diagnostics = append(diagnostics, diags...)
|
||||
|
||||
key, diags := VisitExpression(n.Key, pre, post)
|
||||
diagnostics = append(diagnostics, diags...)
|
||||
|
||||
value, diags := VisitExpression(n.Value, pre, post)
|
||||
diagnostics = append(diagnostics, diags...)
|
||||
|
||||
condition, diags := VisitExpression(n.Condition, pre, post)
|
||||
diagnostics = append(diagnostics, diags...)
|
||||
|
||||
n.Collection, n.Key, n.Value, n.Condition = collection, key, value, condition
|
||||
|
||||
expr, diags := post(n)
|
||||
return expr, append(diagnostics, diags...)
|
||||
}
|
||||
|
||||
func visitFunctionCall(n *FunctionCallExpression, pre, post ExpressionVisitor) (Expression, hcl.Diagnostics) {
|
||||
var diagnostics hcl.Diagnostics
|
||||
|
||||
args, diags := visitExpressions(n.Args, pre, post)
|
||||
diagnostics = append(diagnostics, diags...)
|
||||
|
||||
n.Args = args
|
||||
|
||||
expr, diags := post(n)
|
||||
return expr, append(diagnostics, diags...)
|
||||
}
|
||||
|
||||
func visitIndex(n *IndexExpression, pre, post ExpressionVisitor) (Expression, hcl.Diagnostics) {
|
||||
var diagnostics hcl.Diagnostics
|
||||
|
||||
collection, diags := VisitExpression(n.Collection, pre, post)
|
||||
diagnostics = append(diagnostics, diags...)
|
||||
|
||||
key, diags := VisitExpression(n.Key, pre, post)
|
||||
diagnostics = append(diagnostics, diags...)
|
||||
|
||||
n.Collection, n.Key = collection, key
|
||||
|
||||
expr, diags := post(n)
|
||||
return expr, append(diagnostics, diags...)
|
||||
}
|
||||
|
||||
func visitObjectCons(n *ObjectConsExpression, pre, post ExpressionVisitor) (Expression, hcl.Diagnostics) {
|
||||
var diagnostics hcl.Diagnostics
|
||||
|
||||
for i, item := range n.Items {
|
||||
key, diags := VisitExpression(item.Key, pre, post)
|
||||
diagnostics = append(diagnostics, diags...)
|
||||
|
||||
value, diags := VisitExpression(item.Value, pre, post)
|
||||
diagnostics = append(diagnostics, diags...)
|
||||
|
||||
n.Items[i] = ObjectConsItem{Key: key, Value: value}
|
||||
}
|
||||
|
||||
expr, diags := post(n)
|
||||
return expr, append(diagnostics, diags...)
|
||||
}
|
||||
|
||||
func visitRelativeTraversal(n *RelativeTraversalExpression, pre, post ExpressionVisitor) (Expression, hcl.Diagnostics) {
|
||||
var diagnostics hcl.Diagnostics
|
||||
|
||||
source, diags := VisitExpression(n.Source, pre, post)
|
||||
diagnostics = append(diagnostics, diags...)
|
||||
|
||||
n.Source = source
|
||||
|
||||
expr, diags := post(n)
|
||||
return expr, append(diagnostics, diags...)
|
||||
}
|
||||
|
||||
func visitSplat(n *SplatExpression, pre, post ExpressionVisitor) (Expression, hcl.Diagnostics) {
|
||||
var diagnostics hcl.Diagnostics
|
||||
|
||||
source, diags := VisitExpression(n.Source, pre, post)
|
||||
diagnostics = append(diagnostics, diags...)
|
||||
|
||||
each, diags := VisitExpression(n.Each, pre, post)
|
||||
diagnostics = append(diagnostics, diags...)
|
||||
|
||||
n.Source, n.Each = source, each
|
||||
|
||||
expr, diags := post(n)
|
||||
return expr, append(diagnostics, diags...)
|
||||
}
|
||||
|
||||
func visitTemplate(n *TemplateExpression, pre, post ExpressionVisitor) (Expression, hcl.Diagnostics) {
|
||||
var diagnostics hcl.Diagnostics
|
||||
|
||||
parts, diags := visitExpressions(n.Parts, pre, post)
|
||||
diagnostics = append(diagnostics, diags...)
|
||||
|
||||
n.Parts = parts
|
||||
|
||||
expr, diags := post(n)
|
||||
return expr, append(diagnostics, diags...)
|
||||
}
|
||||
|
||||
func visitTemplateJoin(n *TemplateJoinExpression, pre, post ExpressionVisitor) (Expression, hcl.Diagnostics) {
|
||||
var diagnostics hcl.Diagnostics
|
||||
|
||||
tuple, diags := VisitExpression(n.Tuple, pre, post)
|
||||
diagnostics = append(diagnostics, diags...)
|
||||
|
||||
n.Tuple = tuple
|
||||
|
||||
expr, diags := post(n)
|
||||
return expr, append(diagnostics, diags...)
|
||||
}
|
||||
|
||||
func visitTupleCons(n *TupleConsExpression, pre, post ExpressionVisitor) (Expression, hcl.Diagnostics) {
|
||||
var diagnostics hcl.Diagnostics
|
||||
|
||||
expressions, diags := visitExpressions(n.Expressions, pre, post)
|
||||
diagnostics = append(diagnostics, diags...)
|
||||
|
||||
n.Expressions = expressions
|
||||
|
||||
expr, diags := post(n)
|
||||
return expr, append(diagnostics, diags...)
|
||||
}
|
||||
|
||||
func visitUnaryOp(n *UnaryOpExpression, pre, post ExpressionVisitor) (Expression, hcl.Diagnostics) {
|
||||
var diagnostics hcl.Diagnostics
|
||||
|
||||
operand, diags := VisitExpression(n.Operand, pre, post)
|
||||
diagnostics = append(diagnostics, diags...)
|
||||
|
||||
n.Operand = operand
|
||||
|
||||
expr, diags := post(n)
|
||||
return expr, append(diagnostics, diags...)
|
||||
}
|
||||
|
||||
func visitExpressions(ns []Expression, pre, post ExpressionVisitor) ([]Expression, hcl.Diagnostics) {
|
||||
var diagnostics hcl.Diagnostics
|
||||
|
||||
nils := 0
|
||||
for i, e := range ns {
|
||||
ee, diags := VisitExpression(e, pre, post)
|
||||
diagnostics = append(diagnostics, diags...)
|
||||
if ee == nil {
|
||||
nils++
|
||||
}
|
||||
ns[i] = ee
|
||||
}
|
||||
if nils == 0 {
|
||||
return ns, diagnostics
|
||||
} else if nils == len(ns) {
|
||||
return []Expression{}, diagnostics
|
||||
}
|
||||
|
||||
nns := make([]Expression, 0, len(ns)-nils)
|
||||
for _, e := range ns {
|
||||
if e != nil {
|
||||
nns = append(nns, e)
|
||||
}
|
||||
}
|
||||
return nns, diagnostics
|
||||
}
|
||||
|
||||
// VisitExpression visits each node in an expression tree using the given pre- and post-order visitors. If the preorder
|
||||
// visitor returns a new node, that node's descendents will be visited. VisitExpression returns the result of the
|
||||
// post-order visitor. All diagnostics are accumulated.
|
||||
func VisitExpression(n Expression, pre, post ExpressionVisitor) (Expression, hcl.Diagnostics) {
|
||||
if n == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
nn, preDiags := pre(n)
|
||||
|
||||
var postDiags hcl.Diagnostics
|
||||
switch n := nn.(type) {
|
||||
case *AnonymousFunctionExpression:
|
||||
nn, postDiags = visitAnonymousFunction(n, pre, post)
|
||||
case *BinaryOpExpression:
|
||||
nn, postDiags = visitBinaryOp(n, pre, post)
|
||||
case *ConditionalExpression:
|
||||
nn, postDiags = visitConditional(n, pre, post)
|
||||
case *ErrorExpression:
|
||||
nn, postDiags = post(n)
|
||||
case *ForExpression:
|
||||
nn, postDiags = visitFor(n, pre, post)
|
||||
case *FunctionCallExpression:
|
||||
nn, postDiags = visitFunctionCall(n, pre, post)
|
||||
case *IndexExpression:
|
||||
nn, postDiags = visitIndex(n, pre, post)
|
||||
case *LiteralValueExpression:
|
||||
nn, postDiags = post(n)
|
||||
case *ObjectConsExpression:
|
||||
nn, postDiags = visitObjectCons(n, pre, post)
|
||||
case *RelativeTraversalExpression:
|
||||
nn, postDiags = visitRelativeTraversal(n, pre, post)
|
||||
case *ScopeTraversalExpression:
|
||||
nn, postDiags = post(n)
|
||||
case *SplatExpression:
|
||||
nn, postDiags = visitSplat(n, pre, post)
|
||||
case *TemplateExpression:
|
||||
nn, postDiags = visitTemplate(n, pre, post)
|
||||
case *TemplateJoinExpression:
|
||||
nn, postDiags = visitTemplateJoin(n, pre, post)
|
||||
case *TupleConsExpression:
|
||||
nn, postDiags = visitTupleCons(n, pre, post)
|
||||
case *UnaryOpExpression:
|
||||
nn, postDiags = visitUnaryOp(n, pre, post)
|
||||
default:
|
||||
contract.Failf("unexpected node type in visitExpression: %T", n)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return nn, append(preDiags, postDiags...)
|
||||
}
|
|
@ -282,7 +282,7 @@ func (l tokenList) offsetIndex(offset int) int {
|
|||
case r.Start.Byte <= offset && offset < r.End.Byte:
|
||||
return base + i
|
||||
case r.End.Byte <= offset:
|
||||
l, base = l[i:], base+i
|
||||
l, base = l[i+1:], base+i+1
|
||||
default:
|
||||
contract.Failf("unexpected index condition: %v, %v, %v", r.Start.Byte, r.End.Byte, offset)
|
||||
}
|
||||
|
@ -316,7 +316,7 @@ func (l tokenList) inRange(r hcl.Range) []Token {
|
|||
|
||||
// A TokenMap is used to map from syntax nodes to information about their tokens and leading whitespace/comments.
|
||||
type TokenMap interface {
|
||||
ForNode(n hclsyntax.Node) (NodeTokens, bool)
|
||||
ForNode(n hclsyntax.Node) NodeTokens
|
||||
|
||||
isTokenMap()
|
||||
}
|
||||
|
@ -324,9 +324,8 @@ type TokenMap interface {
|
|||
type tokenMap map[hclsyntax.Node]NodeTokens
|
||||
|
||||
// ForNode returns the token information for the given node, if any.
|
||||
func (m tokenMap) ForNode(n hclsyntax.Node) (NodeTokens, bool) {
|
||||
tokens, ok := m[n]
|
||||
return tokens, ok
|
||||
func (m tokenMap) ForNode(n hclsyntax.Node) NodeTokens {
|
||||
return m[n]
|
||||
}
|
||||
|
||||
func (tokenMap) isTokenMap() {}
|
||||
|
@ -344,7 +343,7 @@ func NewTokenMapForFiles(files []*File) TokenMap {
|
|||
|
||||
// mapTokens builds a mapping from the syntax nodes in the given source file to their tokens. The mapping is recorded
|
||||
// in the map passed in to the function.
|
||||
func mapTokens(rawTokens hclsyntax.Tokens, filename string, file *hcl.File, tokenMap tokenMap) {
|
||||
func mapTokens(rawTokens hclsyntax.Tokens, filename string, root hclsyntax.Node, contents []byte, tokenMap tokenMap) {
|
||||
// Turn the list of raw tokens into a list of trivia-carrying tokens.
|
||||
var lastEndPos hcl.Pos
|
||||
var tokens tokenList
|
||||
|
@ -352,7 +351,7 @@ func mapTokens(rawTokens hclsyntax.Tokens, filename string, file *hcl.File, toke
|
|||
for _, raw := range rawTokens {
|
||||
// Snip whitespace out of the body and turn it in to trivia.
|
||||
if startPos := raw.Range.Start; startPos.Byte != lastEndPos.Byte {
|
||||
triviaBytes := file.Bytes[lastEndPos.Byte:startPos.Byte]
|
||||
triviaBytes := contents[lastEndPos.Byte:startPos.Byte]
|
||||
|
||||
// If this trivia begins a new line, attach the current trivia to the last processed token, if any.
|
||||
if len(tokens) > 0 {
|
||||
|
@ -397,8 +396,7 @@ func mapTokens(rawTokens hclsyntax.Tokens, filename string, file *hcl.File, toke
|
|||
// expression or a token) is used to find the token.
|
||||
//
|
||||
// TODO(pdg): handle parenthesized expressions
|
||||
body := file.Body.(*hclsyntax.Body)
|
||||
diags := hclsyntax.VisitAll(body, func(n hclsyntax.Node) hcl.Diagnostics {
|
||||
diags := hclsyntax.VisitAll(root, func(n hclsyntax.Node) hcl.Diagnostics {
|
||||
var nodeTokens NodeTokens
|
||||
switch n := n.(type) {
|
||||
case *hclsyntax.Attribute:
|
||||
|
@ -538,11 +536,13 @@ func mapTokens(rawTokens hclsyntax.Tokens, filename string, file *hcl.File, toke
|
|||
case *hclsyntax.TupleConsExpr:
|
||||
exprs := n.Exprs
|
||||
commas := make([]Token, 0, len(exprs))
|
||||
for _, ex := range exprs[:len(exprs)-1] {
|
||||
commas = append(commas, tokens.atPos(ex.Range().End))
|
||||
}
|
||||
if trailing := tokens.atPos(exprs[len(exprs)-1].Range().End); trailing.Raw.Type == hclsyntax.TokenComma {
|
||||
commas = append(commas, trailing)
|
||||
if len(exprs) > 0 {
|
||||
for _, ex := range exprs[:len(exprs)-1] {
|
||||
commas = append(commas, tokens.atPos(ex.Range().End))
|
||||
}
|
||||
if trailing := tokens.atPos(exprs[len(exprs)-1].Range().End); trailing.Raw.Type == hclsyntax.TokenComma {
|
||||
commas = append(commas, trailing)
|
||||
}
|
||||
}
|
||||
nodeTokens = TupleConsTokens{
|
||||
OpenBracket: tokens.atPos(n.OpenRange.Start),
|
||||
|
@ -562,8 +562,9 @@ func mapTokens(rawTokens hclsyntax.Tokens, filename string, file *hcl.File, toke
|
|||
})
|
||||
contract.Assert(diags == nil)
|
||||
|
||||
// If there is a trailing end-of-file token (and there should be), attach it to the top-level body.
|
||||
if len(tokens) > 0 && tokens[len(tokens)-1].Raw.Type == hclsyntax.TokenEOF {
|
||||
// If the root was a Body and there is a trailing end-of-file token, attach it to the body.
|
||||
body, isBody := root.(*hclsyntax.Body)
|
||||
if isBody && len(tokens) > 0 && tokens[len(tokens)-1].Raw.Type == hclsyntax.TokenEOF {
|
||||
tokenMap[body] = BodyTokens{EndOfFile: tokens[len(tokens)-1]}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -76,67 +76,54 @@ type validator struct {
|
|||
func (v *validator) Enter(n hclsyntax.Node) hcl.Diagnostics {
|
||||
switch n := n.(type) {
|
||||
case *hclsyntax.Attribute:
|
||||
tokens, _ := v.tokens.ForNode(n)
|
||||
ts := tokens.(AttributeTokens)
|
||||
validateTrivia(v.t, ts.Name, ts.Equals)
|
||||
tokens := v.tokens.ForNode(n).(AttributeTokens)
|
||||
validateTrivia(v.t, tokens.Name, tokens.Equals)
|
||||
case *hclsyntax.BinaryOpExpr:
|
||||
tokens, _ := v.tokens.ForNode(n)
|
||||
ts := tokens.(BinaryOpTokens)
|
||||
validateTrivia(v.t, ts.Operator)
|
||||
tokens := v.tokens.ForNode(n).(BinaryOpTokens)
|
||||
validateTrivia(v.t, tokens.Operator)
|
||||
case *hclsyntax.Block:
|
||||
tokens, _ := v.tokens.ForNode(n)
|
||||
ts := tokens.(BlockTokens)
|
||||
validateTrivia(v.t, ts.Type, ts.Labels, ts.OpenBrace, ts.CloseBrace)
|
||||
tokens := v.tokens.ForNode(n).(BlockTokens)
|
||||
validateTrivia(v.t, tokens.Type, tokens.Labels, tokens.OpenBrace, tokens.CloseBrace)
|
||||
case *hclsyntax.ConditionalExpr:
|
||||
tokens, _ := v.tokens.ForNode(n)
|
||||
ts := tokens.(ConditionalTokens)
|
||||
validateTrivia(v.t, ts.QuestionMark, ts.Colon)
|
||||
tokens := v.tokens.ForNode(n).(ConditionalTokens)
|
||||
validateTrivia(v.t, tokens.QuestionMark, tokens.Colon)
|
||||
case *hclsyntax.ForExpr:
|
||||
tokens, _ := v.tokens.ForNode(n)
|
||||
ts := tokens.(ForTokens)
|
||||
validateTrivia(v.t, ts.Open, ts.For, ts.Key, ts.Comma, ts.Value, ts.In, ts.Colon, ts.Arrow, ts.Group, ts.If, ts.Close)
|
||||
tokens := v.tokens.ForNode(n).(ForTokens)
|
||||
validateTrivia(v.t, tokens.Open, tokens.For, tokens.Key, tokens.Comma, tokens.Value, tokens.In, tokens.Colon,
|
||||
tokens.Arrow, tokens.Group, tokens.If, tokens.Close)
|
||||
case *hclsyntax.FunctionCallExpr:
|
||||
tokens, _ := v.tokens.ForNode(n)
|
||||
ts := tokens.(FunctionCallTokens)
|
||||
validateTrivia(v.t, ts.Name, ts.OpenParen, ts.CloseParen)
|
||||
tokens := v.tokens.ForNode(n).(FunctionCallTokens)
|
||||
validateTrivia(v.t, tokens.Name, tokens.OpenParen, tokens.CloseParen)
|
||||
case *hclsyntax.IndexExpr:
|
||||
tokens, _ := v.tokens.ForNode(n)
|
||||
ts := tokens.(IndexTokens)
|
||||
validateTrivia(v.t, ts.OpenBracket, ts.CloseBracket)
|
||||
tokens := v.tokens.ForNode(n).(IndexTokens)
|
||||
validateTrivia(v.t, tokens.OpenBracket, tokens.CloseBracket)
|
||||
case *hclsyntax.LiteralValueExpr:
|
||||
// TODO(pdg): validate string literals
|
||||
if !v.inTemplate {
|
||||
tokens, _ := v.tokens.ForNode(n)
|
||||
ts := tokens.(LiteralValueTokens)
|
||||
validateTrivia(v.t, ts.Value)
|
||||
tokens := v.tokens.ForNode(n).(LiteralValueTokens)
|
||||
validateTrivia(v.t, tokens.Value)
|
||||
}
|
||||
case *hclsyntax.ObjectConsExpr:
|
||||
tokens, _ := v.tokens.ForNode(n)
|
||||
ts := tokens.(ObjectConsTokens)
|
||||
validateTrivia(v.t, ts.OpenBrace, ts.Items, ts.CloseBrace)
|
||||
tokens := v.tokens.ForNode(n).(ObjectConsTokens)
|
||||
validateTrivia(v.t, tokens.OpenBrace, tokens.Items, tokens.CloseBrace)
|
||||
case *hclsyntax.RelativeTraversalExpr:
|
||||
tokens, _ := v.tokens.ForNode(n)
|
||||
ts := tokens.(RelativeTraversalTokens)
|
||||
validateTrivia(v.t, ts.Traversal)
|
||||
tokens := v.tokens.ForNode(n).(RelativeTraversalTokens)
|
||||
validateTrivia(v.t, tokens.Traversal)
|
||||
case *hclsyntax.ScopeTraversalExpr:
|
||||
tokens, _ := v.tokens.ForNode(n)
|
||||
ts := tokens.(ScopeTraversalTokens)
|
||||
validateTrivia(v.t, ts.Root, ts.Traversal)
|
||||
tokens := v.tokens.ForNode(n).(ScopeTraversalTokens)
|
||||
validateTrivia(v.t, tokens.Root, tokens.Traversal)
|
||||
case *hclsyntax.SplatExpr:
|
||||
tokens, _ := v.tokens.ForNode(n)
|
||||
ts := tokens.(SplatTokens)
|
||||
validateTrivia(v.t, ts.Open, ts.Star, ts.Close)
|
||||
tokens := v.tokens.ForNode(n).(SplatTokens)
|
||||
validateTrivia(v.t, tokens.Open, tokens.Star, tokens.Close)
|
||||
case *hclsyntax.TemplateExpr:
|
||||
// TODO(pdg): validate template tokens.
|
||||
v.inTemplate = true
|
||||
case *hclsyntax.TupleConsExpr:
|
||||
tokens, _ := v.tokens.ForNode(n)
|
||||
ts := tokens.(TupleConsTokens)
|
||||
validateTrivia(v.t, ts.OpenBracket, ts.Commas, ts.CloseBracket)
|
||||
tokens := v.tokens.ForNode(n).(TupleConsTokens)
|
||||
validateTrivia(v.t, tokens.OpenBracket, tokens.Commas, tokens.CloseBracket)
|
||||
case *hclsyntax.UnaryOpExpr:
|
||||
tokens, _ := v.tokens.ForNode(n)
|
||||
ts := tokens.(UnaryOpTokens)
|
||||
validateTrivia(v.t, ts.Operator)
|
||||
tokens := v.tokens.ForNode(n).(UnaryOpTokens)
|
||||
validateTrivia(v.t, tokens.Operator)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -53,7 +53,7 @@ func (p *Parser) ParseFile(r io.Reader, filename string) error {
|
|||
hclFile, diags := hclsyntax.ParseConfig(src, filename, hcl.Pos{})
|
||||
if !diags.HasErrors() {
|
||||
tokens, _ := hclsyntax.LexConfig(src, filename, hcl.Pos{})
|
||||
mapTokens(tokens, filename, hclFile, p.tokens)
|
||||
mapTokens(tokens, filename, hclFile.Body.(*hclsyntax.Body), hclFile.Bytes, p.tokens)
|
||||
}
|
||||
|
||||
p.Files = append(p.Files, &File{
|
||||
|
@ -79,3 +79,16 @@ func NewDiagnosticWriter(w io.Writer, files []*File, width uint, color bool) hcl
|
|||
}
|
||||
return hcl.NewDiagnosticTextWriter(w, fileMap, width, color)
|
||||
}
|
||||
|
||||
// ParseExpression attempts to parse the given string as an HCL2 expression.
|
||||
func ParseExpression(expression, filename string, start hcl.Pos) (hclsyntax.Expression, TokenMap, hcl.Diagnostics) {
|
||||
source := []byte(expression)
|
||||
hclExpression, diagnostics := hclsyntax.ParseExpression(source, filename, start)
|
||||
if diagnostics.HasErrors() {
|
||||
return nil, nil, diagnostics
|
||||
}
|
||||
tokens := tokenMap{}
|
||||
hclTokens, _ := hclsyntax.LexExpression(source, filename, start)
|
||||
mapTokens(hclTokens, filename, hclExpression, source, tokens)
|
||||
return hclExpression, tokens, diagnostics
|
||||
}
|
||||
|
|
6
pkg/codegen/hcl2/syntax/utilities.go
Normal file
6
pkg/codegen/hcl2/syntax/utilities.go
Normal file
|
@ -0,0 +1,6 @@
|
|||
package syntax
|
||||
|
||||
import "github.com/hashicorp/hcl/v2/hclsyntax"
|
||||
|
||||
// None is an HCL syntax node that can be used when a syntax node is required but none is appropriate.
|
||||
var None hclsyntax.Node = &hclsyntax.Body{}
|
|
@ -32,12 +32,6 @@ import (
|
|||
"github.com/pulumi/pulumi/sdk/go/common/util/contract"
|
||||
)
|
||||
|
||||
// IsInternalPropertyKey returns true if the given property key is an internal key that should not be displayed to
|
||||
// users.
|
||||
func IsInternalPropertyKey(key resource.PropertyKey) bool {
|
||||
return strings.HasPrefix(string(key), "__")
|
||||
}
|
||||
|
||||
// GetIndent computes a step's parent indentation.
|
||||
func GetIndent(step StepEventMetadata, seen map[resource.URN]StepEventMetadata) int {
|
||||
indent := 0
|
||||
|
@ -231,7 +225,7 @@ func PrintObject(
|
|||
|
||||
// Now print out the values intelligently based on the type.
|
||||
for _, k := range keys {
|
||||
if v := props[k]; !IsInternalPropertyKey(k) && shouldPrintPropertyValue(v, planning) {
|
||||
if v := props[k]; !resource.IsInternalPropertyKey(k) && shouldPrintPropertyValue(v, planning) {
|
||||
printPropertyTitle(b, string(k), maxkey, indent, op, prefix)
|
||||
printPropertyValue(b, v, planning, indent, op, prefix, debug)
|
||||
}
|
||||
|
@ -355,7 +349,7 @@ func GetResourceOutputsPropertiesString(
|
|||
// the new outputs, we want to print the diffs.
|
||||
var outputDiff *resource.ObjectDiff
|
||||
if step.Old != nil && step.Old.Outputs != nil {
|
||||
outputDiff = step.Old.Outputs.Diff(outs, IsInternalPropertyKey)
|
||||
outputDiff = step.Old.Outputs.Diff(outs, resource.IsInternalPropertyKey)
|
||||
|
||||
// If this is the root stack type, we want to strip out any nested resource outputs that are not known if
|
||||
// they have no corresponding output in the old state.
|
||||
|
@ -387,9 +381,9 @@ func GetResourceOutputsPropertiesString(
|
|||
// - a property with the same key is not present in the inputs
|
||||
// - the property that is present in the inputs is different
|
||||
// - we are doing a refresh, in which case we always want to show state differences
|
||||
if outputDiff != nil || (!IsInternalPropertyKey(k) && shouldPrintPropertyValue(out, true)) {
|
||||
if outputDiff != nil || (!resource.IsInternalPropertyKey(k) && shouldPrintPropertyValue(out, true)) {
|
||||
if in, has := ins[k]; has && !refresh {
|
||||
if out.Diff(in, IsInternalPropertyKey) == nil {
|
||||
if out.Diff(in, resource.IsInternalPropertyKey) == nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
@ -551,7 +545,7 @@ func printOldNewDiffs(
|
|||
planning bool, indent int, op deploy.StepOp, summary bool, debug bool) {
|
||||
|
||||
// Get the full diff structure between the two, and print it (recursively).
|
||||
if diff := olds.Diff(news, IsInternalPropertyKey); diff != nil {
|
||||
if diff := olds.Diff(news, resource.IsInternalPropertyKey); diff != nil {
|
||||
PrintObjectDiff(b, *diff, include, planning, indent, summary, debug)
|
||||
} else {
|
||||
// If there's no diff, report the op as Same - there's no diff to render
|
||||
|
|
|
@ -44,6 +44,7 @@ require (
|
|||
github.com/spf13/cobra v0.0.6
|
||||
github.com/stretchr/testify v1.5.1
|
||||
github.com/xeipuuv/gojsonschema v1.2.0
|
||||
github.com/zclconf/go-cty v1.2.0
|
||||
gocloud.dev v0.19.0
|
||||
gocloud.dev/secrets/hashivault v0.19.0
|
||||
golang.org/x/crypto v0.0.0-20200317142112-1b76d66859c6
|
||||
|
|
|
@ -1737,18 +1737,15 @@ func (pt *ProgramTester) prepareGoProject(projinfo *engine.Projinfo) error {
|
|||
}
|
||||
|
||||
for _, pkg := range pt.opts.Dependencies {
|
||||
err = pt.runCommand("vendor-rm-dep",
|
||||
[]string{"/bin/rm", "-rf", filepath.Join(".", "vendor", pkg)}, cwd)
|
||||
err = os.RemoveAll(filepath.Join(cwd, "vendor", pkg))
|
||||
if err != nil {
|
||||
return fmt.Errorf("error installing go dependencies: %w", err)
|
||||
}
|
||||
err = pt.runCommand("vendor-cp-dep",
|
||||
[]string{"/bin/cp", "-r", filepath.Join(gopath, "src", pkg), filepath.Join(".", "vendor", pkg)}, cwd)
|
||||
err = CopyDir(filepath.Join(gopath, "src", pkg), filepath.Join(cwd, "vendor", pkg))
|
||||
if err != nil {
|
||||
return fmt.Errorf("error installing go dependencies: %w", err)
|
||||
}
|
||||
err = pt.runCommand("vendor-rm-dep-vendor",
|
||||
[]string{"/bin/rm", "-rf", filepath.Join(".", "vendor", pkg, "vendor")}, cwd)
|
||||
err = os.RemoveAll(filepath.Join(cwd, "vendor", pkg, "vendor"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("error installing go dependencies: %w", err)
|
||||
}
|
||||
|
|
|
@ -16,10 +16,12 @@ package integration
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
@ -134,3 +136,65 @@ func (prefixer *prefixer) Write(p []byte) (int, error) {
|
|||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// CopyFile copies a single file from src to dst
|
||||
// From https://blog.depado.eu/post/copy-files-and-directories-in-go
|
||||
func CopyFile(src, dst string) error {
|
||||
var err error
|
||||
var srcfd *os.File
|
||||
var dstfd *os.File
|
||||
var srcinfo os.FileInfo
|
||||
|
||||
if srcfd, err = os.Open(src); err != nil {
|
||||
return err
|
||||
}
|
||||
defer srcfd.Close()
|
||||
|
||||
if dstfd, err = os.Create(dst); err != nil {
|
||||
return err
|
||||
}
|
||||
defer dstfd.Close()
|
||||
|
||||
if _, err = io.Copy(dstfd, srcfd); err != nil {
|
||||
return err
|
||||
}
|
||||
if srcinfo, err = os.Stat(src); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Chmod(dst, srcinfo.Mode())
|
||||
}
|
||||
|
||||
// CopyDir copies a whole directory recursively
|
||||
// From https://blog.depado.eu/post/copy-files-and-directories-in-go
|
||||
func CopyDir(src, dst string) error {
|
||||
var err error
|
||||
var fds []os.FileInfo
|
||||
var srcinfo os.FileInfo
|
||||
|
||||
if srcinfo, err = os.Stat(src); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = os.MkdirAll(dst, srcinfo.Mode()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if fds, err = ioutil.ReadDir(src); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, fd := range fds {
|
||||
srcfp := path.Join(src, fd.Name())
|
||||
dstfp := path.Join(dst, fd.Name())
|
||||
|
||||
if fd.IsDir() {
|
||||
if err = CopyDir(srcfp, dstfp); err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
} else {
|
||||
if err = CopyFile(srcfp, dstfp); err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -37,6 +37,7 @@ CopyPackage "$Root\sdk\nodejs\bin" "pulumi"
|
|||
Copy-Item "$Root\sdk\nodejs\dist\pulumi-resource-pulumi-nodejs.cmd" "$PublishDir\bin"
|
||||
Copy-Item "$Root\sdk\python\dist\pulumi-resource-pulumi-python.cmd" "$PublishDir\bin"
|
||||
Copy-Item "$Root\sdk\nodejs\dist\pulumi-analyzer-policy.cmd" "$PublishDir\bin"
|
||||
Copy-Item "$Root\sdk\python\dist\pulumi-analyzer-policy-python.cmd" "$PublishDir\bin"
|
||||
Copy-Item "$Root\sdk\python\cmd\pulumi-language-python-exec" "$PublishDir\bin"
|
||||
|
||||
# By default, if the archive already exists, 7zip will just add files to it, so blow away the existing
|
||||
|
|
|
@ -56,6 +56,7 @@ run_go_build "${ROOT}/sdk/go/pulumi-language-go"
|
|||
cp "${ROOT}/sdk/nodejs/dist/pulumi-resource-pulumi-nodejs" "${PUBDIR}/bin/"
|
||||
cp "${ROOT}/sdk/python/dist/pulumi-resource-pulumi-python" "${PUBDIR}/bin/"
|
||||
cp "${ROOT}/sdk/nodejs/dist/pulumi-analyzer-policy" "${PUBDIR}/bin/"
|
||||
cp "${ROOT}/sdk/python/dist/pulumi-analyzer-policy-python" "${PUBDIR}/bin/"
|
||||
cp "${ROOT}/sdk/python/cmd/pulumi-language-python-exec" "${PUBDIR}/bin/"
|
||||
|
||||
# Copy packages
|
||||
|
|
|
@ -18,6 +18,7 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
|
@ -78,12 +79,22 @@ func NewAnalyzer(host Host, ctx *Context, name tokens.QName) (Analyzer, error) {
|
|||
}, nil
|
||||
}
|
||||
|
||||
const policyAnalyzerName = "policy"
|
||||
|
||||
// NewPolicyAnalyzer boots the nodejs analyzer plugin located at `policyPackpath`
|
||||
func NewPolicyAnalyzer(
|
||||
host Host, ctx *Context, name tokens.QName, policyPackPath string, opts *PolicyAnalyzerOptions) (Analyzer, error) {
|
||||
|
||||
proj, err := workspace.LoadPolicyPack(filepath.Join(policyPackPath, "PulumiPolicy.yaml"))
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to load Pulumi policy project located at %q", policyPackPath)
|
||||
}
|
||||
|
||||
// For historical reasons, the Node.js plugin name is just "policy".
|
||||
// All other languages have the runtime appended, e.g. "policy-<runtime>".
|
||||
policyAnalyzerName := "policy"
|
||||
if !strings.EqualFold(proj.Runtime.Name(), "nodejs") {
|
||||
policyAnalyzerName = fmt.Sprintf("policy-%s", proj.Runtime.Name())
|
||||
}
|
||||
|
||||
// Load the policy-booting analyzer plugin (i.e., `pulumi-analyzer-${policyAnalyzerName}`).
|
||||
_, pluginPath, err := workspace.GetPluginPath(
|
||||
workspace.AnalyzerPlugin, policyAnalyzerName, nil)
|
||||
|
@ -97,7 +108,7 @@ func NewPolicyAnalyzer(
|
|||
}
|
||||
|
||||
// Create the environment variables from the options.
|
||||
env, err := constructEnv(opts)
|
||||
env, err := constructEnv(opts, proj.Runtime.Name())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -145,7 +156,8 @@ func (a *analyzer) Analyze(r AnalyzerResource) ([]AnalyzeDiagnostic, error) {
|
|||
|
||||
label := fmt.Sprintf("%s.Analyze(%s)", a.label(), t)
|
||||
logging.V(7).Infof("%s executing (#props=%d)", label, len(props))
|
||||
mprops, err := MarshalProperties(props, MarshalOptions{KeepUnknowns: true, KeepSecrets: true})
|
||||
mprops, err := MarshalProperties(props,
|
||||
MarshalOptions{KeepUnknowns: true, KeepSecrets: true, SkipInternalKeys: true})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -185,7 +197,8 @@ func (a *analyzer) AnalyzeStack(resources []AnalyzerStackResource) ([]AnalyzeDia
|
|||
|
||||
protoResources := make([]*pulumirpc.AnalyzerResource, len(resources))
|
||||
for idx, resource := range resources {
|
||||
props, err := MarshalProperties(resource.Properties, MarshalOptions{KeepUnknowns: true, KeepSecrets: true})
|
||||
props, err := MarshalProperties(resource.Properties,
|
||||
MarshalOptions{KeepUnknowns: true, KeepSecrets: true, SkipInternalKeys: true})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "marshalling properties")
|
||||
}
|
||||
|
@ -406,7 +419,8 @@ func marshalProvider(provider *AnalyzerProviderResource) (*pulumirpc.AnalyzerPro
|
|||
return nil, nil
|
||||
}
|
||||
|
||||
props, err := MarshalProperties(provider.Properties, MarshalOptions{KeepUnknowns: true, KeepSecrets: true})
|
||||
props, err := MarshalProperties(provider.Properties,
|
||||
MarshalOptions{KeepUnknowns: true, KeepSecrets: true, SkipInternalKeys: true})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "marshalling properties")
|
||||
}
|
||||
|
@ -597,7 +611,7 @@ func convertDiagnostics(protoDiagnostics []*pulumirpc.AnalyzeDiagnostic) ([]Anal
|
|||
// constructEnv creates a slice of key/value pairs to be used as the environment for the policy pack process. Each entry
|
||||
// is of the form "key=value". Config is passed as an environment variable (including unecrypted secrets), similar to
|
||||
// how config is passed to each language runtime plugin.
|
||||
func constructEnv(opts *PolicyAnalyzerOptions) ([]string, error) {
|
||||
func constructEnv(opts *PolicyAnalyzerOptions, runtime string) ([]string, error) {
|
||||
env := os.Environ()
|
||||
|
||||
maybeAppendEnv := func(k, v string) {
|
||||
|
@ -613,9 +627,19 @@ func constructEnv(opts *PolicyAnalyzerOptions) ([]string, error) {
|
|||
maybeAppendEnv("PULUMI_CONFIG", config)
|
||||
|
||||
if opts != nil {
|
||||
maybeAppendEnv("PULUMI_NODEJS_PROJECT", opts.Project)
|
||||
maybeAppendEnv("PULUMI_NODEJS_STACK", opts.Stack)
|
||||
maybeAppendEnv("PULUMI_NODEJS_DRY_RUN", fmt.Sprintf("%v", opts.DryRun))
|
||||
// Set both PULUMI_NODEJS_* and PULUMI_* environment variables for Node.js. The Node.js
|
||||
// SDK currently looks for the PULUMI_NODEJS_* variants only, but we'd like to move to
|
||||
// using the more general PULUMI_* variants for all languages to avoid special casing
|
||||
// like this, and setting the PULUMI_* variants for Node.js is the first step.
|
||||
if runtime == "nodejs" {
|
||||
maybeAppendEnv("PULUMI_NODEJS_PROJECT", opts.Project)
|
||||
maybeAppendEnv("PULUMI_NODEJS_STACK", opts.Stack)
|
||||
maybeAppendEnv("PULUMI_NODEJS_DRY_RUN", fmt.Sprintf("%v", opts.DryRun))
|
||||
}
|
||||
|
||||
maybeAppendEnv("PULUMI_PROJECT", opts.Project)
|
||||
maybeAppendEnv("PULUMI_STACK", opts.Stack)
|
||||
maybeAppendEnv("PULUMI_DRY_RUN", fmt.Sprintf("%v", opts.DryRun))
|
||||
}
|
||||
|
||||
return env, nil
|
||||
|
|
|
@ -36,6 +36,7 @@ type MarshalOptions struct {
|
|||
ComputeAssetHashes bool // true if we are computing missing asset hashes on the fly.
|
||||
KeepSecrets bool // true if we are keeping secrets (otherwise we replace them with their underlying value).
|
||||
RejectAssets bool // true if we should return errors on Asset and Archive values.
|
||||
SkipInternalKeys bool // true to skip internal property keys (keys that start with "__") in the resulting map.
|
||||
}
|
||||
|
||||
const (
|
||||
|
@ -72,6 +73,8 @@ func MarshalProperties(props resource.PropertyMap, opts MarshalOptions) (*struct
|
|||
logging.V(9).Infof("Skipping output property for RPC[%s]: %v", opts.Label, key)
|
||||
} else if opts.SkipNulls && v.IsNull() {
|
||||
logging.V(9).Infof("Skipping null property for RPC[%s]: %s (as requested)", opts.Label, key)
|
||||
} else if opts.SkipInternalKeys && resource.IsInternalPropertyKey(key) {
|
||||
logging.V(9).Infof("Skipping internal property for RPC[%s]: %s (as requested)", opts.Label, key)
|
||||
} else {
|
||||
m, err := MarshalPropertyValue(v, opts)
|
||||
if err != nil {
|
||||
|
@ -222,6 +225,8 @@ func UnmarshalProperties(props *structpb.Struct, opts MarshalOptions) (resource.
|
|||
logging.V(9).Infof("Unmarshaling property for RPC[%s]: %s=%v", opts.Label, key, v)
|
||||
if opts.SkipNulls && v.IsNull() {
|
||||
logging.V(9).Infof("Skipping unmarshaling for RPC[%s]: %s is null", opts.Label, key)
|
||||
} else if opts.SkipInternalKeys && resource.IsInternalPropertyKey(pk) {
|
||||
logging.V(9).Infof("Skipping unmarshaling for RPC[%s]: %s is internal", opts.Label, key)
|
||||
} else {
|
||||
result[pk] = *v
|
||||
}
|
||||
|
|
|
@ -261,3 +261,27 @@ func TestUnknownSig(t *testing.T) {
|
|||
assert.Error(t, err)
|
||||
|
||||
}
|
||||
|
||||
func TestSkipInternalKeys(t *testing.T) {
|
||||
opts := MarshalOptions{SkipInternalKeys: true}
|
||||
expected := &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"keepers": {
|
||||
Kind: &structpb.Value_StructValue{
|
||||
StructValue: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
props := resource.NewPropertyMapFromMap(map[string]interface{}{
|
||||
"__defaults": []string{},
|
||||
"keepers": map[string]interface{}{
|
||||
"__defaults": []string{},
|
||||
},
|
||||
})
|
||||
actual, err := MarshalProperties(props, opts)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected, actual)
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ import (
|
|||
"fmt"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/pulumi/pulumi/sdk/go/common/tokens"
|
||||
"github.com/pulumi/pulumi/sdk/go/common/util/contract"
|
||||
|
@ -546,3 +547,9 @@ func HasSig(obj PropertyMap, match string) bool {
|
|||
|
||||
// SecretSig is the unique secret signature.
|
||||
const SecretSig = "1b47061264138c4ac30d75fd1eb44270"
|
||||
|
||||
// IsInternalPropertyKey returns true if the given property key is an internal key that should not be displayed to
|
||||
// users.
|
||||
func IsInternalPropertyKey(key PropertyKey) bool {
|
||||
return strings.HasPrefix(string(key), "__")
|
||||
}
|
||||
|
|
|
@ -37,6 +37,76 @@ import (
|
|||
pulumirpc "github.com/pulumi/pulumi/sdk/proto/go"
|
||||
)
|
||||
|
||||
const unableToFindProgramTemplate = "unable to find program: %s"
|
||||
|
||||
// findExecutable attempts to find the needed executable in various locations on the
|
||||
// filesystem, eventually resorting to searching in $PATH.
|
||||
func findExecutable(program string) (string, error) {
|
||||
if runtime.GOOS == "windows" {
|
||||
program = fmt.Sprintf("%s.exe", program)
|
||||
}
|
||||
// look in the same directory
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "unable to get current working directory")
|
||||
}
|
||||
|
||||
cwdProgram := filepath.Join(cwd, program)
|
||||
if fileInfo, err := os.Stat(cwdProgram); !os.IsNotExist(err) && !fileInfo.Mode().IsDir() {
|
||||
logging.V(5).Infof("program %s found in CWD", program)
|
||||
return cwdProgram, nil
|
||||
}
|
||||
|
||||
// look in $GOPATH/bin
|
||||
if goPath := os.Getenv("GOPATH"); len(goPath) > 0 {
|
||||
goPathProgram := filepath.Join(goPath, "bin", program)
|
||||
if fileInfo, err := os.Stat(goPathProgram); !os.IsNotExist(err) && !fileInfo.Mode().IsDir() {
|
||||
logging.V(5).Infof("program %s found in $GOPATH/bin", program)
|
||||
return goPathProgram, nil
|
||||
}
|
||||
}
|
||||
|
||||
// look in the $PATH somewhere
|
||||
if fullPath, err := exec.LookPath(program); err == nil {
|
||||
logging.V(5).Infof("program %s found in $PATH", program)
|
||||
return fullPath, nil
|
||||
}
|
||||
|
||||
return "", errors.Errorf(unableToFindProgramTemplate, program)
|
||||
}
|
||||
|
||||
func findProgram(project string) (*exec.Cmd, error) {
|
||||
// The program to execute is simply the name of the project. This ensures good Go toolability, whereby
|
||||
// you can simply run `go install .` to build a Pulumi program prior to running it, among other benefits.
|
||||
// For ease of use, if we don't find a pre-built program, we attempt to invoke via 'go run' on behalf of the user.
|
||||
program, err := findExecutable(project)
|
||||
if err == nil {
|
||||
return exec.Command(program), nil
|
||||
}
|
||||
|
||||
const message = "problem executing program (could not run language executor)"
|
||||
if err.Error() == fmt.Sprintf(unableToFindProgramTemplate, project) {
|
||||
logging.V(5).Infof("Unable to find program %s in $PATH, attempting invocation via 'go run'", program)
|
||||
program, err = findExecutable("go")
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, message)
|
||||
}
|
||||
|
||||
// Fall back to 'go run' style execution
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unable to get current working directory")
|
||||
}
|
||||
|
||||
goFileSearchPattern := filepath.Join(cwd, "*.go")
|
||||
if matches, err := filepath.Glob(goFileSearchPattern); err != nil || len(matches) == 0 {
|
||||
return nil, errors.Errorf("Failed to find go files for 'go run' matching %s", goFileSearchPattern)
|
||||
}
|
||||
|
||||
return exec.Command(program, "run", cwd), nil
|
||||
}
|
||||
|
||||
// Launches the language host, which in turn fires up an RPC server implementing the LanguageRuntimeServer endpoint.
|
||||
func main() {
|
||||
var tracing string
|
||||
|
@ -90,46 +160,34 @@ func newLanguageHost(engineAddress, tracing string) pulumirpc.LanguageRuntimeSer
|
|||
// GetRequiredPlugins computes the complete set of anticipated plugins required by a program.
|
||||
func (host *goLanguageHost) GetRequiredPlugins(ctx context.Context,
|
||||
req *pulumirpc.GetRequiredPluginsRequest) (*pulumirpc.GetRequiredPluginsResponse, error) {
|
||||
return &pulumirpc.GetRequiredPluginsResponse{}, nil
|
||||
}
|
||||
|
||||
const unableToFindProgramTemplate = "unable to find program: %s"
|
||||
|
||||
// findProgram attempts to find the needed program in various locations on the
|
||||
// filesystem, eventually resorting to searching in $PATH.
|
||||
func findProgram(program string) (string, error) {
|
||||
if runtime.GOOS == "windows" {
|
||||
program = fmt.Sprintf("%s.exe", program)
|
||||
}
|
||||
|
||||
// look in the same directory
|
||||
cwd, err := os.Getwd()
|
||||
cmd, err := findProgram(req.GetProject())
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "unable to get current working directory")
|
||||
return nil, errors.Wrap(err, "failed to find program")
|
||||
}
|
||||
|
||||
cwdProgram := filepath.Join(cwd, program)
|
||||
if fileInfo, err := os.Stat(cwdProgram); !os.IsNotExist(err) && !fileInfo.Mode().IsDir() {
|
||||
logging.V(5).Infof("program %s found in CWD", program)
|
||||
return cwdProgram, nil
|
||||
cmd.Env = os.Environ()
|
||||
cmd.Env = append(cmd.Env, "PULUMI_PLUGINS=true")
|
||||
|
||||
stdout, err := cmd.Output()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to execute program cmd")
|
||||
}
|
||||
|
||||
// look in $GOPATH/bin
|
||||
if goPath := os.Getenv("GOPATH"); len(goPath) > 0 {
|
||||
goPathProgram := filepath.Join(goPath, "bin", program)
|
||||
if fileInfo, err := os.Stat(goPathProgram); !os.IsNotExist(err) && !fileInfo.Mode().IsDir() {
|
||||
logging.V(5).Infof("program %s found in $GOPATH/bin", program)
|
||||
return goPathProgram, nil
|
||||
}
|
||||
var infos map[string][]pulumi.PackageInfo
|
||||
if err := json.Unmarshal(stdout, &infos); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to unmarshal result")
|
||||
}
|
||||
|
||||
// look in the $PATH somewhere
|
||||
if fullPath, err := exec.LookPath(program); err == nil {
|
||||
logging.V(5).Infof("program %s found in $PATH", program)
|
||||
return fullPath, nil
|
||||
var plugins []*pulumirpc.PluginDependency
|
||||
for _, info := range infos["plugins"] {
|
||||
plugins = append(plugins, &pulumirpc.PluginDependency{
|
||||
Name: info.Name,
|
||||
Kind: "resource",
|
||||
Version: info.Version,
|
||||
Server: info.Server,
|
||||
})
|
||||
}
|
||||
|
||||
return "", errors.Errorf(unableToFindProgramTemplate, program)
|
||||
return &pulumirpc.GetRequiredPluginsResponse{Plugins: plugins}, nil
|
||||
}
|
||||
|
||||
// RPC endpoint for LanguageRuntimeServer::Run
|
||||
|
@ -141,54 +199,14 @@ func (host *goLanguageHost) Run(ctx context.Context, req *pulumirpc.RunRequest)
|
|||
return nil, errors.Wrap(err, "failed to prepare environment")
|
||||
}
|
||||
|
||||
// by default we try to run a named executable on the path, but we will fallback to 'go run' style execution
|
||||
goRunInvoke := false
|
||||
|
||||
// The program to execute is simply the name of the project. This ensures good Go toolability, whereby
|
||||
// you can simply run `go install .` to build a Pulumi program prior to running it, among other benefits.
|
||||
// For ease of use, if we don't find a pre-built program, we attempt to invoke via 'go run' on behalf of the user.
|
||||
program, err := findProgram(req.GetProject())
|
||||
cmd, err := findProgram(req.GetProject())
|
||||
if err != nil {
|
||||
const message = "problem executing program (could not run language executor)"
|
||||
if err.Error() == fmt.Sprintf(unableToFindProgramTemplate, req.GetProject()) {
|
||||
logging.V(5).Infof("Unable to find program %s in $PATH, attempting invocation via 'go run'", program)
|
||||
program, err = findProgram("go")
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, message)
|
||||
}
|
||||
goRunInvoke = true
|
||||
} else {
|
||||
return nil, errors.Wrap(err, message)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logging.V(5).Infof("language host launching process: %s", program)
|
||||
|
||||
// Now simply spawn a process to execute the requested program, wiring up stdout/stderr directly.
|
||||
var errResult string
|
||||
var cmd *exec.Cmd
|
||||
|
||||
if goRunInvoke {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unable to get current working directory")
|
||||
}
|
||||
|
||||
goFileSearchPattern := filepath.Join(cwd, "*.go")
|
||||
if matches, err := filepath.Glob(goFileSearchPattern); err != nil || len(matches) == 0 {
|
||||
return nil, errors.Errorf("Failed to find go files for 'go run' matching %s", goFileSearchPattern)
|
||||
}
|
||||
|
||||
args := []string{"run", cwd}
|
||||
// go run $cwd
|
||||
cmd = exec.Command(program, args...)
|
||||
} else {
|
||||
cmd = exec.Command(program)
|
||||
}
|
||||
|
||||
cmd.Env = env
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
|
||||
|
||||
var errResult string
|
||||
if err := cmd.Run(); err != nil {
|
||||
if exiterr, ok := err.(*exec.ExitError); ok {
|
||||
// If the program ran, but exited with a non-zero error code. This will happen often, since user
|
||||
|
|
|
@ -27,6 +27,8 @@ import (
|
|||
"github.com/pulumi/pulumi/sdk/go/common/util/contract"
|
||||
)
|
||||
|
||||
var ErrPlugins = errors.New("pulumi: plugins requested")
|
||||
|
||||
// A RunOption is used to control the behavior of Run and RunErr.
|
||||
type RunOption func(*RunInfo)
|
||||
|
||||
|
@ -35,8 +37,13 @@ type RunOption func(*RunInfo)
|
|||
// If the program fails, the process will be terminated and the function will not return.
|
||||
func Run(body RunFunc, opts ...RunOption) {
|
||||
if err := RunErr(body, opts...); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: program failed: %v\n", err)
|
||||
os.Exit(1)
|
||||
if err != ErrPlugins {
|
||||
fmt.Fprintf(os.Stderr, "error: program failed: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
printRequiredPlugins()
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -46,6 +53,9 @@ func RunErr(body RunFunc, opts ...RunOption) error {
|
|||
// Parse the info out of environment variables. This is a lame contract with the caller, but helps to keep
|
||||
// boilerplate to a minimum in the average Pulumi Go program.
|
||||
info := getEnvInfo()
|
||||
if info.getPlugins {
|
||||
return ErrPlugins
|
||||
}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&info)
|
||||
|
@ -121,6 +131,7 @@ type RunInfo struct {
|
|||
MonitorAddr string
|
||||
EngineAddr string
|
||||
Mocks MockResourceMonitor
|
||||
getPlugins bool
|
||||
}
|
||||
|
||||
// getEnvInfo reads various program information from the process environment.
|
||||
|
@ -128,6 +139,7 @@ func getEnvInfo() RunInfo {
|
|||
// Most of the variables are just strings, and we can read them directly. A few of them require more parsing.
|
||||
parallel, _ := strconv.Atoi(os.Getenv(EnvParallel))
|
||||
dryRun, _ := strconv.ParseBool(os.Getenv(EnvDryRun))
|
||||
getPlugins, _ := strconv.ParseBool(os.Getenv(envPlugins))
|
||||
|
||||
var config map[string]string
|
||||
if cfg := os.Getenv(EnvConfig); cfg != "" {
|
||||
|
@ -142,6 +154,7 @@ func getEnvInfo() RunInfo {
|
|||
DryRun: dryRun,
|
||||
MonitorAddr: os.Getenv(EnvMonitor),
|
||||
EngineAddr: os.Getenv(EnvEngine),
|
||||
getPlugins: getPlugins,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -160,4 +173,28 @@ const (
|
|||
EnvMonitor = "PULUMI_MONITOR"
|
||||
// EnvEngine is the envvar used to read the current Pulumi engine RPC address.
|
||||
EnvEngine = "PULUMI_ENGINE"
|
||||
// envPlugins is the envvar used to request that the Pulumi program print its set of required plugins and exit.
|
||||
envPlugins = "PULUMI_PLUGINS"
|
||||
)
|
||||
|
||||
type PackageInfo struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version,omitempty"`
|
||||
Server string `json:"server,omitempty"`
|
||||
}
|
||||
|
||||
var packageRegistry = map[PackageInfo]struct{}{}
|
||||
|
||||
func RegisterPackage(info PackageInfo) {
|
||||
packageRegistry[info] = struct{}{}
|
||||
}
|
||||
|
||||
func printRequiredPlugins() {
|
||||
plugins := []PackageInfo{}
|
||||
for info := range packageRegistry {
|
||||
plugins = append(plugins, info)
|
||||
}
|
||||
|
||||
err := json.NewEncoder(os.Stdout).Encode(map[string]interface{}{"plugins": plugins})
|
||||
contract.IgnoreError(err)
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ lint::
|
|||
install_package::
|
||||
cp ./cmd/pulumi-language-python-exec "$(PULUMI_BIN)"
|
||||
cp ./dist/pulumi-resource-pulumi-python "$(PULUMI_BIN)"
|
||||
cp ./dist/pulumi-analyzer-policy-python "$(PULUMI_BIN)"
|
||||
|
||||
install_plugin::
|
||||
GOBIN=$(PULUMI_BIN) go install \
|
||||
|
@ -46,3 +47,4 @@ dist::
|
|||
go install -ldflags "-X github.com/pulumi/pulumi/sdk/go/common/version.Version=${VERSION}" ${LANGHOST_PKG}
|
||||
cp ./cmd/pulumi-language-python-exec "$$(go env GOPATH)"/bin/
|
||||
cp ./dist/pulumi-resource-pulumi-python "$$(go env GOPATH)"/bin/
|
||||
cp ./dist/pulumi-analyzer-policy-python "$$(go env GOPATH)"/bin/
|
||||
|
|
2
sdk/python/dist/pulumi-analyzer-policy-python
vendored
Executable file
2
sdk/python/dist/pulumi-analyzer-policy-python
vendored
Executable file
|
@ -0,0 +1,2 @@
|
|||
#!/bin/sh
|
||||
python3 -u -m pulumi.policy $@
|
5
sdk/python/dist/pulumi-analyzer-policy-python.cmd
vendored
Executable file
5
sdk/python/dist/pulumi-analyzer-policy-python.cmd
vendored
Executable file
|
@ -0,0 +1,5 @@
|
|||
@echo off
|
||||
setlocal
|
||||
REM We use `python` instead of `python3` because Windows Python installers
|
||||
REM install only `python.exe` by default.
|
||||
@python -u -m pulumi.policy %*
|
|
@ -19,7 +19,7 @@ resources.
|
|||
"""
|
||||
|
||||
# Make subpackages available.
|
||||
__all__ = ['runtime', 'dynamic']
|
||||
__all__ = ['runtime', 'dynamic', 'policy']
|
||||
|
||||
# Make all module members inside of this package available as package members.
|
||||
from .asset import (
|
||||
|
|
17
sdk/python/lib/pulumi/policy/__init__.py
Normal file
17
sdk/python/lib/pulumi/policy/__init__.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
# 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.
|
||||
|
||||
"""
|
||||
The Pulumi SDK's policy module. This is meant for internal use only.
|
||||
"""
|
65
sdk/python/lib/pulumi/policy/__main__.py
Normal file
65
sdk/python/lib/pulumi/policy/__main__.py
Normal file
|
@ -0,0 +1,65 @@
|
|||
# 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.
|
||||
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
import runpy
|
||||
|
||||
import pulumi
|
||||
import pulumi.runtime
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 3:
|
||||
# For whatever reason, sys.stderr.write is not picked up by the engine as a message, but 'print' is. The Python
|
||||
# langhost automatically flushes stdout and stderr on shutdown, so we don't need to do it here - just trust that
|
||||
# Python does the sane thing when printing to stderr.
|
||||
print("usage: python3 -u -m pulumi.policy <engine-address> <program>", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
program = sys.argv[2]
|
||||
|
||||
# If any config variables are present, parse and set them, so subsequent accesses are fast.
|
||||
config_env = pulumi.runtime.get_config_env()
|
||||
for k, v in config_env.items():
|
||||
pulumi.runtime.set_config(k, v)
|
||||
|
||||
# Configure the runtime so that the user program hooks up to Pulumi as appropriate.
|
||||
if 'PULUMI_PROJECT' in os.environ and 'PULUMI_STACK' in os.environ and 'PULUMI_DRY_RUN' in os.environ:
|
||||
pulumi.runtime.configure(
|
||||
pulumi.runtime.Settings(
|
||||
project=os.environ["PULUMI_PROJECT"],
|
||||
stack=os.environ["PULUMI_STACK"],
|
||||
dry_run=os.environ["PULUMI_DRY_RUN"] == "true"
|
||||
)
|
||||
)
|
||||
|
||||
successful = False
|
||||
|
||||
try:
|
||||
runpy.run_path(program, run_name="__main__")
|
||||
successful = True
|
||||
except Exception:
|
||||
pulumi.log.error("Program failed with an unhandled exception:")
|
||||
pulumi.log.error(traceback.format_exc())
|
||||
finally:
|
||||
sys.stdout.flush()
|
||||
sys.stderr.flush()
|
||||
|
||||
exit_code = 0 if successful else 1
|
||||
sys.exit(exit_code)
|
||||
|
||||
|
||||
main()
|
Loading…
Reference in a new issue