pulumi/pkg/codegen/hcl2/binder.go
Pat Gavlin 21634768aa [hcl2] Fix package loading and trivia detection
- Load packages referenced by all node types, not just resource nodes
- Fix trivia detection
2020-05-01 11:00:58 -07:00

251 lines
6.9 KiB
Go

// 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 hcl2
import (
"os"
"sort"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/pulumi/pulumi/pkg/v2/codegen/hcl2/model"
"github.com/pulumi/pulumi/pkg/v2/codegen/hcl2/syntax"
"github.com/pulumi/pulumi/pkg/v2/codegen/schema"
"github.com/pulumi/pulumi/sdk/v2/go/common/resource/plugin"
"github.com/pulumi/pulumi/sdk/v2/go/common/util/contract"
"github.com/zclconf/go-cty/cty"
)
type bindOptions struct {
allowMissingVariables bool
host plugin.Host
packageCache *PackageCache
}
func (opts bindOptions) modelOptions() []model.BindOption {
if opts.allowMissingVariables {
return []model.BindOption{model.AllowMissingVariables}
}
return nil
}
type binder struct {
options bindOptions
referencedPackages []*schema.Package
typeSchemas map[model.Type]schema.Type
tokens syntax.TokenMap
nodes []Node
root *model.Scope
}
type BindOption func(*bindOptions)
func AllowMissingVariables(options *bindOptions) {
options.allowMissingVariables = true
}
func PluginHost(host plugin.Host) BindOption {
return func(options *bindOptions) {
options.host = host
}
}
func Cache(cache *PackageCache) BindOption {
return func(options *bindOptions) {
options.packageCache = cache
}
}
// BindProgram performs semantic analysis on the given set of HCL2 files that represent a single program. The given
// host, if any, is used for loading any resource plugins necessary to extract schema information.
func BindProgram(files []*syntax.File, opts ...BindOption) (*Program, hcl.Diagnostics, error) {
var options bindOptions
for _, o := range opts {
o(&options)
}
if options.host == nil {
cwd, err := os.Getwd()
if err != nil {
return nil, nil, err
}
ctx, err := plugin.NewContext(nil, nil, nil, nil, cwd, nil, nil)
if err != nil {
return nil, nil, err
}
options.host = ctx.Host
defer contract.IgnoreClose(ctx)
}
if options.packageCache == nil {
options.packageCache = NewPackageCache()
}
b := &binder{
options: options,
tokens: syntax.NewTokenMapForFiles(files),
typeSchemas: map[model.Type]schema.Type{},
root: model.NewRootScope(syntax.None),
}
// Define null.
b.root.Define("null", &model.Constant{
Name: "null",
ConstantValue: cty.NullVal(cty.DynamicPseudoType),
})
// Define builtin functions.
for name, fn := range pulumiBuiltins {
b.root.DefineFunction(name, fn)
}
// Define the invoke function.
b.root.DefineFunction("invoke", model.NewFunction(model.GenericFunctionSignature(b.bindInvokeSignature)))
var diagnostics hcl.Diagnostics
// Sort files in source order, then declare all top-level nodes in each.
sort.Slice(files, func(i, j int) bool {
return files[i].Name < files[j].Name
})
for _, f := range files {
fileDiags, err := b.declareNodes(f)
if err != nil {
return nil, nil, err
}
diagnostics = append(diagnostics, fileDiags...)
}
// Now bind the nodes.
for _, n := range b.nodes {
diagnostics = append(diagnostics, b.bindNode(n)...)
}
return &Program{
Nodes: b.nodes,
files: files,
binder: b,
}, diagnostics, nil
}
// declareNodes declares all of the top-level nodes in the given file. This invludes config, resources, outputs, and
// locals.
func (b *binder) declareNodes(file *syntax.File) (hcl.Diagnostics, error) {
var diagnostics hcl.Diagnostics
// Declare body items in source order.
for _, item := range model.SourceOrderBody(file.Body) {
switch item := item.(type) {
case *hclsyntax.Attribute:
v := &LocalVariable{syntax: item}
attrDiags := b.declareNode(item.Name, v)
diagnostics = append(diagnostics, attrDiags...)
if err := b.loadReferencedPackageSchemas(v); err != nil {
return nil, err
}
case *hclsyntax.Block:
switch item.Type {
case "config":
name, typ := "<unnamed>", model.Type(model.DynamicType)
switch len(item.Labels) {
case 1:
name = item.Labels[0]
case 2:
name = item.Labels[0]
typeExpr, diags := model.BindExpressionText(item.Labels[1], model.TypeScope, item.LabelRanges[1].Start)
diagnostics = append(diagnostics, diags...)
typ = typeExpr.Type()
default:
diagnostics = append(diagnostics, labelsErrorf(item, "config variables must have exactly one or two labels"))
}
// TODO(pdg): check body for valid contents
v := &ConfigVariable{
typ: typ,
syntax: item,
}
diags := b.declareNode(name, v)
diagnostics = append(diagnostics, diags...)
if err := b.loadReferencedPackageSchemas(v); err != nil {
return nil, err
}
case "resource":
if len(item.Labels) != 2 {
diagnostics = append(diagnostics, labelsErrorf(item, "resource variables must have exactly two labels"))
}
resource := &Resource{
syntax: item,
}
declareDiags := b.declareNode(item.Labels[0], resource)
diagnostics = append(diagnostics, declareDiags...)
if err := b.loadReferencedPackageSchemas(resource); err != nil {
return nil, err
}
case "output":
name, typ := "<unnamed>", model.Type(model.DynamicType)
switch len(item.Labels) {
case 1:
name = item.Labels[0]
case 2:
name = item.Labels[0]
typeExpr, diags := model.BindExpressionText(item.Labels[1], model.TypeScope, item.LabelRanges[1].Start)
diagnostics = append(diagnostics, diags...)
typ = typeExpr.Type()
default:
diagnostics = append(diagnostics, labelsErrorf(item, "config variables must have exactly one or two labels"))
}
// TODO(pdg): check body for valid contents
v := &OutputVariable{
typ: typ,
syntax: item,
}
diags := b.declareNode(name, v)
diagnostics = append(diagnostics, diags...)
if err := b.loadReferencedPackageSchemas(v); err != nil {
return nil, err
}
}
}
}
return diagnostics, nil
}
// declareNode declares a single top-level node. If a node with the same name has already been declared, it returns an
// appropriate diagnostic.
func (b *binder) declareNode(name string, n Node) hcl.Diagnostics {
if !b.root.Define(name, n) {
existing, _ := b.root.BindReference(name)
return hcl.Diagnostics{errorf(existing.SyntaxNode().Range(), "%q already declared", name)}
}
b.nodes = append(b.nodes, n)
return nil
}
func (b *binder) bindExpression(node hclsyntax.Node) (model.Expression, hcl.Diagnostics) {
return model.BindExpression(node, b.root, b.tokens, b.options.modelOptions()...)
}