pulumi/pkg/codegen/hcl2/model/scope.go

314 lines
9.7 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 model
import (
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/syntax"
"github.com/zclconf/go-cty/cty"
)
// 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
}
func (v *Variable) Value(context *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
if value, hasValue := context.Variables[v.Name]; hasValue {
return value, nil
}
return cty.DynamicVal, nil
}
// A Constant is a traversable, typed definition that represents a named constant.
type Constant struct {
// The syntax node associated with the constant definition, if any.
Syntax hclsyntax.Node
// The name of the constant.
Name string
// The value of the constant.
ConstantValue cty.Value
typ Type
}
// Traverse attempts to traverse the constant's value.
func (c *Constant) Traverse(traverser hcl.Traverser) (Traversable, hcl.Diagnostics) {
v, diags := traverser.TraversalStep(c.ConstantValue)
return &Constant{ConstantValue: v}, diags
}
// SyntaxNode returns the constant's syntax node or syntax.None.
func (c *Constant) SyntaxNode() hclsyntax.Node {
return syntaxOrNone(c.Syntax)
}
// Type returns the type of the constant.
func (c *Constant) Type() Type {
if c.typ == nil {
if c.ConstantValue.IsNull() {
c.typ = NoneType
} else {
c.typ = ctyTypeToType(c.ConstantValue.Type(), false)
}
}
return c.typ
}
func (c *Constant) Value(context *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
return c.ConstantValue, nil
}
// A Scope is used to map names to definitions during expression binding.
//
// A scope has three namespaces:
// - one that is exclusive to functions
// - one that is exclusive to output variables
// - and one that contains config variables, local variables and resource definitions.
//
// When binding a reference, we check `defs` and `outputs`. When binding a function, we check `functions`.
// Definitions within a namespace such as `defs`, `outputs` or `functions` are expected to have a unique identifier
// and cannot be redeclared.
type Scope struct {
parent *Scope
syntax hclsyntax.Node
defs map[string]Definition
outputs 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{},
outputs: 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 {
// TODO(pdg): return a better error here
return DynamicType, hcl.Diagnostics{undefinedVariable("", traverser.SourceRange(), false)}
}
memberName := name.AsString()
member, hasMember := s.BindReference(memberName)
if !hasMember {
return DynamicType, hcl.Diagnostics{undefinedVariable(memberName, traverser.SourceRange(), false)}
}
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 {
// first we check inside defs which include config variables, local variables and resource definitions
if def, ok := s.defs[name]; ok {
return def, true
}
// then we check the namespace of the outputs.
// it is unlikely that an output is being referenced by something but still possible
// that is why we check outputs after checking defs
if output, ok := s.outputs[name]; ok {
return output, 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
}
// isOutputDefinition returns whether the input definition is an output variable
func isOutputDefinition(def Definition) bool {
syntax := def.SyntaxNode()
if syntax != nil {
switch syntax := syntax.(type) {
case *hclsyntax.Block:
return syntax.Type == "output"
}
}
return 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 isOutputDefinition(def) {
// if the definition we have is an output
// then we only check inside the outputs namespace
if _, hasDef := s.outputs[name]; !hasDef {
s.outputs[name] = def
return true
}
} else {
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{},
outputs: 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{},
outputs: 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}
}