mirror of
https://github.com/opentffoundation/opentf.git
synced 2025-12-19 17:59:05 -05:00
626 lines
23 KiB
Go
626 lines
23 KiB
Go
// Copyright (c) The OpenTofu Authors
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
// Copyright (c) 2023 HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package json
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/hcl/v2"
|
|
"github.com/hashicorp/hcl/v2/hcled"
|
|
"github.com/hashicorp/hcl/v2/hclparse"
|
|
"github.com/hashicorp/hcl/v2/hclsyntax"
|
|
"github.com/zclconf/go-cty/cty"
|
|
|
|
"github.com/opentofu/opentofu/internal/lang/marks"
|
|
"github.com/opentofu/opentofu/internal/tfdiags"
|
|
)
|
|
|
|
// These severities map to the tfdiags.Severity values, plus an explicit
|
|
// unknown in case that enum grows without us noticing here.
|
|
const (
|
|
DiagnosticSeverityUnknown = "unknown"
|
|
DiagnosticSeverityError = "error"
|
|
DiagnosticSeverityWarning = "warning"
|
|
)
|
|
|
|
// Diagnostic represents any tfdiags.Diagnostic value. The simplest form has
|
|
// just a severity, single line summary, and optional detail. If there is more
|
|
// information about the source of the diagnostic, this is represented in the
|
|
// range field.
|
|
type Diagnostic struct {
|
|
Severity string `json:"severity"`
|
|
Summary string `json:"summary"`
|
|
Detail string `json:"detail"`
|
|
Address string `json:"address,omitempty"`
|
|
Range *DiagnosticRange `json:"range,omitempty"`
|
|
Snippet *DiagnosticSnippet `json:"snippet,omitempty"`
|
|
}
|
|
|
|
// Pos represents a position in the source code.
|
|
type Pos struct {
|
|
// Line is a one-based count for the line in the indicated file.
|
|
Line int `json:"line"`
|
|
|
|
// Column is a one-based count of Unicode characters from the start of the line.
|
|
Column int `json:"column"`
|
|
|
|
// Byte is a zero-based offset into the indicated file.
|
|
Byte int `json:"byte"`
|
|
}
|
|
|
|
// DiagnosticRange represents the filename and position of the diagnostic
|
|
// subject. This defines the range of the source to be highlighted in the
|
|
// output. Note that the snippet may include additional surrounding source code
|
|
// if the diagnostic has a context range.
|
|
//
|
|
// The Start position is inclusive, and the End position is exclusive. Exact
|
|
// positions are intended for highlighting for human interpretation only and
|
|
// are subject to change.
|
|
type DiagnosticRange struct {
|
|
Filename string `json:"filename"`
|
|
Start Pos `json:"start"`
|
|
End Pos `json:"end"`
|
|
}
|
|
|
|
// DiagnosticSnippet represents source code information about the diagnostic.
|
|
// It is possible for a diagnostic to have a source (and therefore a range) but
|
|
// no source code can be found. In this case, the range field will be present and
|
|
// the snippet field will not.
|
|
type DiagnosticSnippet struct {
|
|
// Context is derived from HCL's hcled.ContextString output. This gives a
|
|
// high-level summary of the root context of the diagnostic: for example,
|
|
// the resource block in which an expression causes an error.
|
|
Context *string `json:"context"`
|
|
|
|
// Code is a possibly-multi-line string of OpenTofu configuration, which
|
|
// includes both the diagnostic source and any relevant context as defined
|
|
// by the diagnostic.
|
|
Code string `json:"code"`
|
|
|
|
// StartLine is the line number in the source file for the first line of
|
|
// the snippet code block. This is not necessarily the same as the value of
|
|
// Range.Start.Line, as it is possible to have zero or more lines of
|
|
// context source code before the diagnostic range starts.
|
|
StartLine int `json:"start_line"`
|
|
|
|
// HighlightStartOffset is the character offset into Code at which the
|
|
// diagnostic source range starts, which ought to be highlighted as such by
|
|
// the consumer of this data.
|
|
HighlightStartOffset int `json:"highlight_start_offset"`
|
|
|
|
// HighlightEndOffset is the character offset into Code at which the
|
|
// diagnostic source range ends.
|
|
HighlightEndOffset int `json:"highlight_end_offset"`
|
|
|
|
// Values is a sorted slice of expression values which may be useful in
|
|
// understanding the source of an error in a complex expression.
|
|
Values []DiagnosticExpressionValue `json:"values"`
|
|
|
|
// FunctionCall is information about a function call whose failure is
|
|
// being reported by this diagnostic, if any.
|
|
FunctionCall *DiagnosticFunctionCall `json:"function_call,omitempty"`
|
|
}
|
|
|
|
// DiagnosticExpressionValue represents an HCL traversal string (e.g.
|
|
// "var.foo") and a statement about its value while the expression was
|
|
// evaluated (e.g. "is a string", "will be known only after apply"). These are
|
|
// intended to help the consumer diagnose why an expression caused a diagnostic
|
|
// to be emitted.
|
|
type DiagnosticExpressionValue struct {
|
|
Traversal string `json:"traversal"`
|
|
Statement string `json:"statement"`
|
|
}
|
|
|
|
// DiagnosticFunctionCall represents a function call whose information is
|
|
// being included as part of a diagnostic snippet.
|
|
type DiagnosticFunctionCall struct {
|
|
// CalledAs is the full name that was used to call this function,
|
|
// potentially including namespace prefixes if the function does not belong
|
|
// to the default function namespace.
|
|
CalledAs string `json:"called_as"`
|
|
|
|
// Signature is a description of the signature of the function that was
|
|
// called, if any. Might be omitted if we're reporting that a call failed
|
|
// because the given function name isn't known, for example.
|
|
Signature *Function `json:"signature,omitempty"`
|
|
}
|
|
|
|
// NewDiagnostic takes a tfdiags.Diagnostic and a map of configuration sources,
|
|
// and returns a [Diagnostic] object as a "UI-flavored" representation of the
|
|
// diagnostic.
|
|
func NewDiagnostic(diag tfdiags.Diagnostic, sources map[string]*hcl.File) *Diagnostic {
|
|
var sev string
|
|
switch diag.Severity() {
|
|
case tfdiags.Error:
|
|
sev = DiagnosticSeverityError
|
|
case tfdiags.Warning:
|
|
sev = DiagnosticSeverityWarning
|
|
default:
|
|
sev = DiagnosticSeverityUnknown
|
|
}
|
|
|
|
sourceRefs := diag.Source()
|
|
highlightRange, snippetRange := prepareDiagnosticRanges(sourceRefs.Subject, sourceRefs.Context)
|
|
|
|
// If the diagnostic has source location information then we will try to construct a snippet
|
|
// showing a relevant portion of the source code.
|
|
snippet := newDiagnosticSnippet(snippetRange, highlightRange, sources)
|
|
if snippet != nil {
|
|
// We might be able to annotate the snippet with some dynamic-expression-related information,
|
|
// if this is a suitably-enriched diagnostic. These are not strictly part of the "snippet",
|
|
// but we return them all together because the human-readable UI presents this information
|
|
// all together as one UI element.
|
|
snippet.Values = newDiagnosticExpressionValues(diag)
|
|
snippet.FunctionCall = newDiagnosticSnippetFunctionCall(diag)
|
|
}
|
|
|
|
desc := diag.Description()
|
|
return &Diagnostic{
|
|
Severity: sev,
|
|
Summary: desc.Summary,
|
|
Detail: desc.Detail,
|
|
Address: desc.Address,
|
|
Range: newDiagnosticRange(highlightRange),
|
|
Snippet: snippet,
|
|
}
|
|
}
|
|
|
|
// prepareDiagnosticRanges takes the raw subject and context source ranges from a
|
|
// diagnostic message and returns the more UI-oriented "highlight" and "snippet"
|
|
// ranges.
|
|
//
|
|
// The "highlight" range describes the characters that are considered to be the
|
|
// direct cause of the problem, and which are typically presented as underlined
|
|
// when producing human-readable diagnostics in a terminal that can support that.
|
|
//
|
|
// The "snippet" range describes a potentially-larger range of characters that
|
|
// should all be included in the source code snippet included in the diagnostic
|
|
// message. The highlight range is guaranteed to be contained within the
|
|
// snippet range. Some of our diagnostic messages use this, for example, to
|
|
// ensure that the whole of an expression gets included in the snippet even if
|
|
// the problem is just one operand of the expression and the expression is wrapped
|
|
// over multiple lines.
|
|
func prepareDiagnosticRanges(subject, context *tfdiags.SourceRange) (highlight, snippet *tfdiags.SourceRange) {
|
|
if subject == nil {
|
|
// If we don't even have a "subject" then we have no ranges to report at all.
|
|
return nil, nil
|
|
}
|
|
|
|
// We'll borrow HCL's range implementation here, because it has some
|
|
// handy features to help us produce a nice source code snippet.
|
|
highlightRange := subject.ToHCL()
|
|
|
|
// Some diagnostic sources fail to set the end of the subject range.
|
|
if highlightRange.End == (hcl.Pos{}) {
|
|
highlightRange.End = highlightRange.Start
|
|
}
|
|
|
|
snippetRange := highlightRange
|
|
if context != nil {
|
|
snippetRange = context.ToHCL()
|
|
}
|
|
|
|
// Make sure the snippet includes the highlight. This should be true
|
|
// for any reasonable diagnostic, but we'll make sure.
|
|
snippetRange = hcl.RangeOver(snippetRange, highlightRange)
|
|
|
|
// Empty ranges result in odd diagnostic output, so extend the end to
|
|
// ensure there's at least one byte in the snippet or highlight.
|
|
if highlightRange.Empty() {
|
|
highlightRange.End.Byte++
|
|
highlightRange.End.Column++
|
|
}
|
|
if snippetRange.Empty() {
|
|
snippetRange.End.Byte++
|
|
snippetRange.End.Column++
|
|
}
|
|
|
|
retHighlight := tfdiags.SourceRangeFromHCL(highlightRange)
|
|
retSnippet := tfdiags.SourceRangeFromHCL(snippetRange)
|
|
return &retHighlight, &retSnippet
|
|
}
|
|
|
|
func newDiagnosticRange(highlightRange *tfdiags.SourceRange) *DiagnosticRange {
|
|
if highlightRange == nil {
|
|
// No particular range to report, then.
|
|
return nil
|
|
}
|
|
|
|
return &DiagnosticRange{
|
|
Filename: highlightRange.Filename,
|
|
Start: Pos{
|
|
Line: highlightRange.Start.Line,
|
|
Column: highlightRange.Start.Column,
|
|
Byte: highlightRange.Start.Byte,
|
|
},
|
|
End: Pos{
|
|
Line: highlightRange.End.Line,
|
|
Column: highlightRange.End.Column,
|
|
Byte: highlightRange.End.Byte,
|
|
},
|
|
}
|
|
}
|
|
|
|
func newDiagnosticSnippet(snippetRange, highlightRange *tfdiags.SourceRange, sources map[string]*hcl.File) *DiagnosticSnippet {
|
|
if snippetRange == nil || highlightRange == nil {
|
|
// There is no code that is relevant to show in a snippet for this diagnostic.
|
|
return nil
|
|
}
|
|
file, ok := sources[snippetRange.Filename]
|
|
if !ok {
|
|
// If we don't have the source code for the file that the snippet is supposed
|
|
// to come from then we can't produce a snippet. (This tends to happen when
|
|
// we're rendering a diagnostic from an unusual location that isn't actually
|
|
// a source file, like an expression entered into the "tofu console" prompt.)
|
|
return nil
|
|
}
|
|
src := file.Bytes
|
|
if src == nil {
|
|
// A file without any source bytes? Weird, but perhaps constructed artificially
|
|
// for testing or for other unusual reasons.
|
|
return nil
|
|
}
|
|
|
|
// If we get this far then we're going to do our best to return at least a minimal
|
|
// snippet, though the level of detail depends on what other information we have
|
|
// available.
|
|
ret := &DiagnosticSnippet{
|
|
StartLine: snippetRange.Start.Line,
|
|
|
|
// Ensure that the default Values struct is an empty array, as this
|
|
// makes consuming the JSON structure easier in most languages.
|
|
Values: []DiagnosticExpressionValue{},
|
|
}
|
|
|
|
// Some callers pass us *hcl.File objects they directly constructed rather than
|
|
// using the HCL parser, in which case they lack the "navigation metadata"
|
|
// that HCL's parsers would generate. We need that metadata to extract the
|
|
// context string below, so we'll make a best effort to obtain that metadata.
|
|
file = tryHCLFileWithNavMetadata(file, snippetRange.Filename)
|
|
|
|
// Some diagnostics may have a useful top-level context to add to
|
|
// the code snippet output. This function needs a file with nav metadata
|
|
// to return a useful result, but it will happily return an empty string
|
|
// if given a file without that metadata.
|
|
contextStr := hcled.ContextString(file, highlightRange.Start.Byte-1)
|
|
if contextStr != "" {
|
|
ret.Context = &contextStr
|
|
}
|
|
|
|
// Build the string of the code snippet, tracking at which byte of
|
|
// the file the snippet starts.
|
|
var codeStartByte int
|
|
sc := hcl.NewRangeScanner(src, highlightRange.Filename, bufio.ScanLines)
|
|
var code strings.Builder
|
|
for sc.Scan() {
|
|
lineRange := sc.Range()
|
|
if lineRange.Overlaps(snippetRange.ToHCL()) {
|
|
if codeStartByte == 0 && code.Len() == 0 {
|
|
codeStartByte = lineRange.Start.Byte
|
|
}
|
|
code.Write(lineRange.SliceBytes(src))
|
|
code.WriteRune('\n')
|
|
}
|
|
}
|
|
codeStr := strings.TrimSuffix(code.String(), "\n")
|
|
ret.Code = codeStr
|
|
|
|
// Calculate the start and end byte of the highlight range relative
|
|
// to the code snippet string.
|
|
start := highlightRange.Start.Byte - codeStartByte
|
|
end := start + (highlightRange.End.Byte - highlightRange.Start.Byte)
|
|
|
|
// We can end up with some quirky results here in edge cases like
|
|
// when a source range starts or ends at a newline character,
|
|
// so we'll cap the results at the bounds of the highlight range
|
|
// so that consumers of this data don't need to contend with
|
|
// out-of-bounds errors themselves.
|
|
if start < 0 {
|
|
start = 0
|
|
} else if start > len(codeStr) {
|
|
start = len(codeStr)
|
|
}
|
|
if end < 0 {
|
|
end = 0
|
|
} else if end > len(codeStr) {
|
|
end = len(codeStr)
|
|
}
|
|
|
|
ret.HighlightStartOffset = start
|
|
ret.HighlightEndOffset = end
|
|
|
|
return ret
|
|
}
|
|
|
|
func newDiagnosticExpressionValues(diag tfdiags.Diagnostic) []DiagnosticExpressionValue {
|
|
fromExpr := diag.FromExpr()
|
|
if fromExpr == nil {
|
|
// no expression-related information on this diagnostic, but our
|
|
// callers always want a non-nil slice in this case because that's
|
|
// friendlier for JSON serialization.
|
|
return make([]DiagnosticExpressionValue, 0)
|
|
}
|
|
|
|
// We may also be able to generate information about the dynamic
|
|
// values of relevant variables at the point of evaluation, then.
|
|
// This is particularly useful for expressions that get evaluated
|
|
// multiple times with different values, such as blocks using
|
|
// "count" and "for_each", or within "for" expressions.
|
|
expr := fromExpr.Expression
|
|
ctx := fromExpr.EvalContext
|
|
vars := expr.Variables()
|
|
values := make([]DiagnosticExpressionValue, 0, len(vars))
|
|
seen := make(map[string]struct{}, len(vars))
|
|
includeUnknown := tfdiags.DiagnosticCausedByUnknown(diag)
|
|
includeSensitive := tfdiags.DiagnosticCausedBySensitive(diag)
|
|
Traversals:
|
|
for _, traversal := range vars {
|
|
for len(traversal) > 1 {
|
|
val, diags := traversal.TraverseAbs(ctx)
|
|
if diags.HasErrors() {
|
|
// Skip anything that generates errors, since we probably
|
|
// already have the same error in our diagnostics set
|
|
// already.
|
|
traversal = traversal[:len(traversal)-1]
|
|
continue
|
|
}
|
|
|
|
traversalStr := traversalStr(traversal)
|
|
if _, exists := seen[traversalStr]; exists {
|
|
continue Traversals // don't show duplicates when the same variable is referenced multiple times
|
|
}
|
|
statement := newDiagnosticSnippetValueDescription(val, includeUnknown, includeSensitive)
|
|
if statement == "" {
|
|
// If we don't have anything to say about this value then we won't include
|
|
// an entry for it at all.
|
|
continue Traversals
|
|
}
|
|
values = append(values, DiagnosticExpressionValue{
|
|
Traversal: traversalStr,
|
|
Statement: statement,
|
|
})
|
|
seen[traversalStr] = struct{}{}
|
|
}
|
|
}
|
|
sort.Slice(values, func(i, j int) bool {
|
|
return values[i].Traversal < values[j].Traversal
|
|
})
|
|
return values
|
|
}
|
|
|
|
func newDiagnosticSnippetFunctionCall(diag tfdiags.Diagnostic) *DiagnosticFunctionCall {
|
|
fromExpr := diag.FromExpr()
|
|
if fromExpr == nil {
|
|
return nil // no expression-related information on this diagnostic
|
|
}
|
|
callInfo := tfdiags.ExtraInfo[hclsyntax.FunctionCallDiagExtra](diag)
|
|
if callInfo == nil || callInfo.CalledFunctionName() == "" {
|
|
return nil // no function call information
|
|
}
|
|
|
|
ctx := fromExpr.EvalContext
|
|
calledAs := callInfo.CalledFunctionName()
|
|
baseName := calledAs
|
|
if idx := strings.LastIndex(baseName, "::"); idx >= 0 {
|
|
baseName = baseName[idx+2:]
|
|
}
|
|
ret := &DiagnosticFunctionCall{
|
|
CalledAs: calledAs,
|
|
}
|
|
if f, ok := ctx.Functions[calledAs]; ok {
|
|
ret.Signature = DescribeFunction(baseName, f)
|
|
}
|
|
return ret
|
|
}
|
|
|
|
func newDiagnosticSnippetValueDescription(val cty.Value, includeUnknown, includeSensitive bool) string {
|
|
switch {
|
|
case val.HasMark(marks.Sensitive):
|
|
// We only mention a sensitive value if the diagnostic
|
|
// we're rendering is explicitly marked as being
|
|
// caused by sensitive values, because otherwise
|
|
// readers tend to be misled into thinking the error
|
|
// is caused by the sensitive value even when it isn't.
|
|
if !includeSensitive {
|
|
return ""
|
|
}
|
|
// Even when we do mention one, we keep it vague
|
|
// in order to minimize the chance of giving away
|
|
// whatever was sensitive about it.
|
|
return "has a sensitive value"
|
|
case !val.IsKnown():
|
|
ty := val.Type()
|
|
// We'll avoid saying anything about unknown or
|
|
// "known after apply" unless the diagnostic is
|
|
// explicitly marked as being caused by unknown
|
|
// values, because otherwise readers tend to be
|
|
// misled into thinking the error is caused by the
|
|
// unknown value even when it isn't.
|
|
if !includeUnknown {
|
|
if ty == cty.DynamicPseudoType {
|
|
return "" // if we can't even name the type then we'll say nothing at all
|
|
}
|
|
// We can at least say what the type is, without mentioning "known after apply" at all
|
|
return fmt.Sprintf("is a %s", ty.FriendlyName())
|
|
}
|
|
switch {
|
|
case ty == cty.DynamicPseudoType:
|
|
return "will be known only after apply" // we don't even know what the type will be
|
|
case ty.IsCollectionType():
|
|
// If the unknown value has collection length refinements then we might at least
|
|
// be able to give some hints about the expected length.
|
|
valRng := val.Range()
|
|
minLen := valRng.LengthLowerBound()
|
|
maxLen := valRng.LengthUpperBound()
|
|
const maxLimit = 1024 // (upper limit is just an arbitrary value to avoid showing distracting large numbers in the UI)
|
|
switch {
|
|
case minLen == maxLen:
|
|
return fmt.Sprintf("is a %s of length %d, known only after apply", ty.FriendlyName(), minLen)
|
|
case minLen != 0 && maxLen <= maxLimit:
|
|
return fmt.Sprintf("is a %s with between %d and %d elements, known only after apply", ty.FriendlyName(), minLen, maxLen)
|
|
case minLen != 0:
|
|
return fmt.Sprintf("is a %s with at least %d elements, known only after apply", ty.FriendlyName(), minLen)
|
|
case maxLen <= maxLimit:
|
|
return fmt.Sprintf("is a %s with up to %d elements, known only after apply", ty.FriendlyName(), maxLen)
|
|
default:
|
|
return fmt.Sprintf("is a %s, known only after apply", ty.FriendlyName())
|
|
}
|
|
default:
|
|
return fmt.Sprintf("is a %s, known only after apply", ty.FriendlyName())
|
|
}
|
|
default:
|
|
return fmt.Sprintf("is %s", compactValueStr(val))
|
|
}
|
|
}
|
|
|
|
// compactValueStr produces a compact, single-line summary of a given value
|
|
// that is suitable for display in the UI.
|
|
//
|
|
// For primitives it returns a full representation, while for more complex
|
|
// types it instead summarizes the type, size, etc to produce something
|
|
// that is hopefully still somewhat useful but not as verbose as a rendering
|
|
// of the entire data structure.
|
|
func compactValueStr(val cty.Value) string {
|
|
// This is a specialized subset of value rendering tailored to producing
|
|
// helpful but concise messages in diagnostics. It is not comprehensive
|
|
// nor intended to be used for other purposes.
|
|
|
|
if val.HasMark(marks.Sensitive) {
|
|
// We check this in here just to make sure, but note that the caller
|
|
// of compactValueStr ought to have already checked this and skipped
|
|
// calling into compactValueStr anyway, so this shouldn't actually
|
|
// be reachable.
|
|
return "(sensitive value)"
|
|
}
|
|
|
|
// WARNING: We've only checked that the value isn't sensitive _shallowly_
|
|
// here, and so we must never show any element values from complex types
|
|
// in here. However, it's fine to show map keys and attribute names because
|
|
// those are never sensitive in isolation: the entire value would be
|
|
// sensitive in that case.
|
|
|
|
ty := val.Type()
|
|
switch {
|
|
case val.IsNull():
|
|
return "null"
|
|
case !val.IsKnown():
|
|
// Should never happen here because we should filter before we get
|
|
// in here, but we'll do something reasonable rather than panic.
|
|
return "(not yet known)"
|
|
case ty == cty.Bool:
|
|
if val.True() {
|
|
return "true"
|
|
}
|
|
return "false"
|
|
case ty == cty.Number:
|
|
bf := val.AsBigFloat()
|
|
return bf.Text('g', 10)
|
|
case ty == cty.String:
|
|
// Go string syntax is not exactly the same as HCL native string syntax,
|
|
// but we'll accept the minor edge-cases where this is different here
|
|
// for now, just to get something reasonable here.
|
|
return fmt.Sprintf("%q", val.AsString())
|
|
case ty.IsCollectionType() || ty.IsTupleType():
|
|
l := val.LengthInt()
|
|
switch l {
|
|
case 0:
|
|
return "empty " + ty.FriendlyName()
|
|
case 1:
|
|
return ty.FriendlyName() + " with 1 element"
|
|
default:
|
|
return fmt.Sprintf("%s with %d elements", ty.FriendlyName(), l)
|
|
}
|
|
case ty.IsObjectType():
|
|
atys := ty.AttributeTypes()
|
|
l := len(atys)
|
|
switch l {
|
|
case 0:
|
|
return "object with no attributes"
|
|
case 1:
|
|
var name string
|
|
for k := range atys {
|
|
name = k
|
|
}
|
|
return fmt.Sprintf("object with 1 attribute %q", name)
|
|
default:
|
|
return fmt.Sprintf("object with %d attributes", l)
|
|
}
|
|
default:
|
|
return ty.FriendlyName()
|
|
}
|
|
}
|
|
|
|
// traversalStr produces a representation of an HCL traversal that is compact,
|
|
// resembles HCL native syntax, and is suitable for display in the UI.
|
|
func traversalStr(traversal hcl.Traversal) string {
|
|
// This is a specialized subset of traversal rendering tailored to
|
|
// producing helpful contextual messages in diagnostics. It is not
|
|
// comprehensive nor intended to be used for other purposes.
|
|
|
|
var buf strings.Builder
|
|
for _, step := range traversal {
|
|
switch tStep := step.(type) {
|
|
case hcl.TraverseRoot:
|
|
buf.WriteString(tStep.Name)
|
|
case hcl.TraverseAttr:
|
|
buf.WriteByte('.')
|
|
buf.WriteString(tStep.Name)
|
|
case hcl.TraverseIndex:
|
|
buf.WriteByte('[')
|
|
if keyTy := tStep.Key.Type(); keyTy.IsPrimitiveType() {
|
|
buf.WriteString(compactValueStr(tStep.Key))
|
|
} else {
|
|
// We'll just use a placeholder for more complex values,
|
|
// since otherwise our result could grow ridiculously long.
|
|
buf.WriteString("...")
|
|
}
|
|
buf.WriteByte(']')
|
|
}
|
|
}
|
|
return buf.String()
|
|
}
|
|
|
|
// tryHCLFileWithNavMetadata takes an hcl.File that might have been directly
|
|
// constructed rather than produced by an HCL parser, and tries to pass it
|
|
// through a suitable HCL parser if it lacks the metadata that an HCL parser
|
|
// would normally add.
|
|
//
|
|
// If parsing would be necessary to produce the metadata but parsing fails
|
|
// then this returns the given file verbatim, so the caller must still be
|
|
// prepared to deal with a file lacking navigation metadata.
|
|
func tryHCLFileWithNavMetadata(file *hcl.File, filename string) *hcl.File {
|
|
if file.Nav != nil {
|
|
// If there's _something_ in this field then we'll assume that
|
|
// an HCL parser put it there. The details of this field are
|
|
// HCL-parser-specific so we don't try to dig any deeper.
|
|
return file
|
|
}
|
|
|
|
// If we have a nil nav then we'll try to construct a fully-fledged
|
|
// file by parsing what we were given. This is best-effort, because
|
|
// the file might well have been lacking navigation metadata due to
|
|
// having been invalid in the first place.
|
|
// Re-parsing a file that might well have already been parsed already
|
|
// earlier is a little wasteful, but we only get here when we're
|
|
// returning diagnostics and so we'd rather do a little extra work
|
|
// if it might allow us to return a better diagnostic.
|
|
parser := hclparse.NewParser()
|
|
var newFile *hcl.File
|
|
if strings.HasSuffix(filename, ".json") {
|
|
newFile, _ = parser.ParseJSON(file.Bytes, filename)
|
|
} else {
|
|
newFile, _ = parser.ParseHCL(file.Bytes, filename)
|
|
}
|
|
if newFile == nil {
|
|
// Our best efforts have failed, then. We'll just return what we had.
|
|
return file
|
|
}
|
|
return newFile
|
|
}
|