terraform/internal/stacks/stackaddrs/reference.go

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

232 lines
7 KiB
Go
Raw Permalink Normal View History

// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1
package stackaddrs
import (
"fmt"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/internal/tfdiags"
)
// Reference describes a reference expression found in the configuration,
// capturing what it referred to and where it was found in source code.
type Reference struct {
Target Referenceable
SourceRange tfdiags.SourceRange
}
// ParseReference raises a raw absolute traversal into a higher-level reference,
// or returns error diagnostics explaining why it cannot.
//
// The returned traversal is a relative traversal covering the remainder of
// the given traversal after the part captured into the returned reference,
// in case the caller wants to do further validation or analysis of the
// subsequent steps.
func ParseReference(traversal hcl.Traversal) (Reference, hcl.Traversal, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
var ret Reference
switch rootName := traversal.RootName(); rootName {
case "var":
name, rng, remain, diags := parseSingleAttrRef(traversal)
ret.Target = InputVariable{Name: name}
ret.SourceRange = tfdiags.SourceRangeFromHCL(rng)
return ret, remain, diags
case "local":
name, rng, remain, diags := parseSingleAttrRef(traversal)
ret.Target = LocalValue{Name: name}
ret.SourceRange = tfdiags.SourceRangeFromHCL(rng)
return ret, remain, diags
case "component":
name, rng, remain, diags := parseSingleAttrRef(traversal)
ret.Target = Component{Name: name}
ret.SourceRange = tfdiags.SourceRangeFromHCL(rng)
return ret, remain, diags
case "stack":
name, rng, remain, diags := parseSingleAttrRef(traversal)
ret.Target = StackCall{Name: name}
ret.SourceRange = tfdiags.SourceRangeFromHCL(rng)
return ret, remain, diags
case "provider":
target, rng, remain, diags := parseProviderRef(traversal)
ret.Target = target
ret.SourceRange = tfdiags.SourceRangeFromHCL(rng)
return ret, remain, diags
case "each", "count":
attrName, rng, remain, diags := parseSingleAttrRef(traversal)
if diags.HasErrors() {
return ret, nil, diags
}
ret.SourceRange = tfdiags.SourceRangeFromHCL(rng)
switch rootName {
case "each":
switch attrName {
case "key":
ret.Target = EachKey
return ret, remain, diags
case "value":
ret.Target = EachValue
return ret, remain, diags
}
case "count":
switch attrName {
case "index":
ret.Target = CountIndex
return ret, remain, diags
}
}
// If we get here then rootName and attrName are not a valid combination.
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Reference to unknown symbol",
Detail: fmt.Sprintf("The object %q has no attribute named %q.", rootName, attrName),
Subject: traversal[1].SourceRange().Ptr(),
})
return ret, nil, diags
case "self":
ret.Target = Self
ret.SourceRange = tfdiags.SourceRangeFromHCL(traversal[0].SourceRange())
return ret, traversal[1:], diags
case "terraform":
attrName, rng, remain, diags := parseSingleAttrRef(traversal)
if diags.HasErrors() {
return ret, nil, diags
}
ret.SourceRange = tfdiags.SourceRangeFromHCL(rng)
switch attrName {
case "applying":
ret.Target = TerraformApplying
return ret, remain, diags
default:
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Reference to unknown symbol",
Detail: fmt.Sprintf("The object %q has no attribute named %q.", rootName, attrName),
Subject: traversal[1].SourceRange().Ptr(),
})
return ret, remain, diags
}
stackeval: Test-only globals Because many stacks language features are dependent on others to do useful work, it's tempting to focus only on integration testing of combinations of features used together. However, we known from our experience with the modules runtime (the "terraform" package) that over time this becomes a huge maintenance burden, because any non-trivial change tends to invalidate hundreds or thousands of integration tests, and because of their broad scope its often hard in retrospect to figure out what exactly a particular test was aiming to test vs. what it was just relying on as a side-effect. To try to minimize these cross-dependencies and thus enable something closer to unit testing, here we introduce a special kind of symbol to the stacks language which is available only to unit tests in this package. "Test-only globals" -- an intentionally-clunky name to avoid squatting on useful names -- can be set as part of the Main object and, when defined, are available for use in all situations where we perform expression evaluation against a stack. The "globals" in the name represents that, unlike just about everything else, they are defined once but available in all stacks in the configuration tree. This design is a tradeoff: it introduces a bunch of extra code that is here entirely to support testing, but hopefully this code is segregated enough from everything else that it's unlikely to change significantly under future maintenance, thereby hopefully minimizing the need for future cross-cutting test maintenance too.
2023-11-06 13:28:39 -05:00
case "_test_only_global":
name, rng, remain, diags := parseSingleAttrRef(traversal)
ret.Target = TestOnlyGlobal{Name: name}
ret.SourceRange = tfdiags.SourceRangeFromHCL(rng)
return ret, remain, diags
default:
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Reference to unknown symbol",
Detail: fmt.Sprintf("There is no symbol %q defined in the current scope.", rootName),
Subject: traversal[0].SourceRange().Ptr(),
})
return ret, nil, diags
}
}
func parseSingleAttrRef(traversal hcl.Traversal) (string, hcl.Range, hcl.Traversal, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
root := traversal.RootName()
rootRange := traversal[0].SourceRange()
if len(traversal) < 2 {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid reference",
Detail: fmt.Sprintf("The %q object cannot be accessed directly. Instead, access one of its attributes.", root),
Subject: &rootRange,
})
return "", hcl.Range{}, nil, diags
}
if attrTrav, ok := traversal[1].(hcl.TraverseAttr); ok {
return attrTrav.Name, hcl.RangeBetween(rootRange, attrTrav.SrcRange), traversal[2:], diags
}
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid reference",
Detail: fmt.Sprintf("The %q object does not support this operation.", root),
Subject: traversal[1].SourceRange().Ptr(),
})
return "", hcl.Range{}, nil, diags
}
func parseProviderRef(traversal hcl.Traversal) (ProviderConfigRef, hcl.Range, hcl.Traversal, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
if len(traversal) < 3 {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid reference",
Detail: "The \"provider\" symbol must be followed by two attribute access operations, selecting a provider type and a provider configuration name.",
Subject: traversal.SourceRange().Ptr(),
})
return ProviderConfigRef{}, hcl.Range{}, nil, diags
}
if typeTrav, ok := traversal[1].(hcl.TraverseAttr); ok {
if nameTrav, ok := traversal[2].(hcl.TraverseAttr); ok {
ret := ProviderConfigRef{
ProviderLocalName: typeTrav.Name,
Name: nameTrav.Name,
}
return ret, traversal.SourceRange(), traversal[3:], diags
} else {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid reference",
Detail: "The \"provider\" object's attributes do not support this operation.",
Subject: traversal[1].SourceRange().Ptr(),
})
}
} else {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid reference",
Detail: "The \"provider\" object does not support this operation.",
Subject: traversal[1].SourceRange().Ptr(),
})
}
return ProviderConfigRef{}, hcl.Range{}, nil, diags
}
func (r Reference) Absolute(stack StackInstance) AbsReference {
return AbsReference{
Stack: stack,
Ref: r,
}
}
// AbsReference is an absolute form of [Reference] that is to be resolved
// in the global scope of a particular stack.
//
// It's not meaningful to use this type for references to objects that exist
// only in a more specific scope, such as each.key, each.value, etc, because
// those would require additional information about exactly which object
// they are being resolved in terms of.
type AbsReference struct {
Stack StackInstance
Ref Reference
}
func (r AbsReference) Target() AbsReferenceable {
return AbsReferenceable{
Stack: r.Stack,
Item: r.Ref.Target,
}
}
func (r AbsReference) SourceRange() tfdiags.SourceRange {
return r.Ref.SourceRange
}