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