Merge branch 'master' of https://github.com/pulumi/pulumi into evan/gomod

This commit is contained in:
evanboyle 2020-03-18 17:57:58 -07:00
commit ec686bbaf6
57 changed files with 6333 additions and 178 deletions

View file

@ -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)

View file

@ -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}

View file

@ -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)">

View file

@ -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.

View file

@ -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

View 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)
}
}
})
}

View file

@ -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{}

View 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
}

View 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
}

View 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)
})
}
}

View 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
}

View 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
}

View 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")
}

View 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() {}

View 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)
}

View 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...)
}

View 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)
}

View 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}
}

View 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.

View 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
}

View 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
}

View 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
}

View 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() {}

View 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() {}

View 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() {}

View 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() {}

View 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() {}

View 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() {}

View 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() {}

View 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() {}

View 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))
}

View 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() {}

View 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() {}

View 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
}

View 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...)
}

View file

@ -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]}
}
}

View file

@ -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
}

View file

@ -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
}

View 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{}

View file

@ -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

View file

@ -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

View file

@ -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)
}

View file

@ -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
}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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
}

View file

@ -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)
}

View file

@ -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), "__")
}

View file

@ -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

View file

@ -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)
}

View file

@ -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/

View file

@ -0,0 +1,2 @@
#!/bin/sh
python3 -u -m pulumi.policy $@

View 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 %*

View file

@ -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 (

View 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.
"""

View 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()