mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
command/views/json: Refactor NewDiagnostic into multiple functions
This function was previously quite long and complex, so this commit splits it into a number of smaller functions. The previous code structure was made more awkward by having to work around all being together in one function -- particularly the part iterating over the values used in an expression -- and so the new layout is quite different and thus the diff is hard to read. However, there are intentionally no test changes in this commit to help us be confident that this has not regressed anything, and the existing unit tests for this component seem quite comprehensive. Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
This commit is contained in:
parent
b03f9635ce
commit
5d03530483
@ -7,7 +7,6 @@ package json
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
@ -16,9 +15,10 @@ import (
|
||||
"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"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
// These severities map to the tfdiags.Severity values, plus an explicit
|
||||
@ -132,7 +132,8 @@ type DiagnosticFunctionCall struct {
|
||||
}
|
||||
|
||||
// NewDiagnostic takes a tfdiags.Diagnostic and a map of configuration sources,
|
||||
// and returns a Diagnostic struct.
|
||||
// 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() {
|
||||
@ -144,269 +145,340 @@ func NewDiagnostic(diag tfdiags.Diagnostic, sources map[string]*hcl.File) *Diagn
|
||||
sev = DiagnosticSeverityUnknown
|
||||
}
|
||||
|
||||
desc := diag.Description()
|
||||
sourceRefs := diag.Source()
|
||||
highlightRange, snippetRange := prepareDiagnosticRanges(sourceRefs.Subject, sourceRefs.Context)
|
||||
|
||||
diagnostic := &Diagnostic{
|
||||
// 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,
|
||||
}
|
||||
|
||||
sourceRefs := diag.Source()
|
||||
if sourceRefs.Subject != 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 := sourceRefs.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 sourceRefs.Context != nil {
|
||||
snippetRange = sourceRefs.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 snippetRange.Empty() {
|
||||
snippetRange.End.Byte++
|
||||
snippetRange.End.Column++
|
||||
}
|
||||
if highlightRange.Empty() {
|
||||
highlightRange.End.Byte++
|
||||
highlightRange.End.Column++
|
||||
}
|
||||
|
||||
diagnostic.Range = &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,
|
||||
},
|
||||
}
|
||||
|
||||
var src []byte
|
||||
if sources != nil {
|
||||
if f, ok := sources[highlightRange.Filename]; ok {
|
||||
src = f.Bytes
|
||||
}
|
||||
}
|
||||
|
||||
// If we have a source file for the diagnostic, we can emit a code
|
||||
// snippet.
|
||||
if src != nil {
|
||||
diagnostic.Snippet = &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{},
|
||||
}
|
||||
|
||||
file, offset := parseRange(src, highlightRange)
|
||||
|
||||
// Some diagnostics may have a useful top-level context to add to
|
||||
// the code snippet output.
|
||||
contextStr := hcled.ContextString(file, offset-1)
|
||||
if contextStr != "" {
|
||||
diagnostic.Snippet.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) {
|
||||
if codeStartByte == 0 && code.Len() == 0 {
|
||||
codeStartByte = lineRange.Start.Byte
|
||||
}
|
||||
code.Write(lineRange.SliceBytes(src))
|
||||
code.WriteRune('\n')
|
||||
}
|
||||
}
|
||||
codeStr := strings.TrimSuffix(code.String(), "\n")
|
||||
diagnostic.Snippet.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)
|
||||
}
|
||||
|
||||
diagnostic.Snippet.HighlightStartOffset = start
|
||||
diagnostic.Snippet.HighlightEndOffset = end
|
||||
|
||||
if fromExpr := diag.FromExpr(); fromExpr != nil {
|
||||
// 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
|
||||
}
|
||||
value := DiagnosticExpressionValue{
|
||||
Traversal: traversalStr,
|
||||
}
|
||||
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 {
|
||||
continue Traversals
|
||||
}
|
||||
// Even when we do mention one, we keep it vague
|
||||
// in order to minimize the chance of giving away
|
||||
// whatever was sensitive about it.
|
||||
value.Statement = "has a sensitive value"
|
||||
case !val.IsKnown():
|
||||
// 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 ty := val.Type(); ty != cty.DynamicPseudoType {
|
||||
if includeUnknown {
|
||||
switch {
|
||||
case ty.IsCollectionType():
|
||||
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:
|
||||
value.Statement = fmt.Sprintf("is a %s of length %d, known only after apply", ty.FriendlyName(), minLen)
|
||||
case minLen != 0 && maxLen <= maxLimit:
|
||||
value.Statement = fmt.Sprintf("is a %s with between %d and %d elements, known only after apply", ty.FriendlyName(), minLen, maxLen)
|
||||
case minLen != 0:
|
||||
value.Statement = fmt.Sprintf("is a %s with at least %d elements, known only after apply", ty.FriendlyName(), minLen)
|
||||
case maxLen <= maxLimit:
|
||||
value.Statement = fmt.Sprintf("is a %s with up to %d elements, known only after apply", ty.FriendlyName(), maxLen)
|
||||
default:
|
||||
value.Statement = fmt.Sprintf("is a %s, known only after apply", ty.FriendlyName())
|
||||
}
|
||||
default:
|
||||
value.Statement = fmt.Sprintf("is a %s, known only after apply", ty.FriendlyName())
|
||||
}
|
||||
} else {
|
||||
value.Statement = fmt.Sprintf("is a %s", ty.FriendlyName())
|
||||
}
|
||||
} else {
|
||||
if !includeUnknown {
|
||||
continue Traversals
|
||||
}
|
||||
value.Statement = "will be known only after apply"
|
||||
}
|
||||
default:
|
||||
value.Statement = fmt.Sprintf("is %s", compactValueStr(val))
|
||||
}
|
||||
values = append(values, value)
|
||||
seen[traversalStr] = struct{}{}
|
||||
}
|
||||
}
|
||||
sort.Slice(values, func(i, j int) bool {
|
||||
return values[i].Traversal < values[j].Traversal
|
||||
})
|
||||
diagnostic.Snippet.Values = values
|
||||
|
||||
if callInfo := tfdiags.ExtraInfo[hclsyntax.FunctionCallDiagExtra](diag); callInfo != nil && callInfo.CalledFunctionName() != "" {
|
||||
calledAs := callInfo.CalledFunctionName()
|
||||
baseName := calledAs
|
||||
if idx := strings.LastIndex(baseName, "::"); idx >= 0 {
|
||||
baseName = baseName[idx+2:]
|
||||
}
|
||||
callInfo := &DiagnosticFunctionCall{
|
||||
CalledAs: calledAs,
|
||||
}
|
||||
if f, ok := ctx.Functions[calledAs]; ok {
|
||||
callInfo.Signature = DescribeFunction(baseName, f)
|
||||
}
|
||||
diagnostic.Snippet.FunctionCall = callInfo
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return diagnostic
|
||||
}
|
||||
|
||||
func parseRange(src []byte, rng hcl.Range) (*hcl.File, int) {
|
||||
filename := rng.Filename
|
||||
offset := rng.Start.Byte
|
||||
|
||||
// We need to re-parse here to get a *hcl.File we can interrogate. This
|
||||
// is not awesome since we presumably already parsed the file earlier too,
|
||||
// but this re-parsing is architecturally simpler than retaining all of
|
||||
// the hcl.File objects and we only do this in the case of an error anyway
|
||||
// so the overhead here is not a big problem.
|
||||
parser := hclparse.NewParser()
|
||||
var file *hcl.File
|
||||
|
||||
// Ignore diagnostics here as there is nothing we can do with them.
|
||||
if strings.HasSuffix(filename, ".json") {
|
||||
file, _ = parser.ParseJSON(src, filename)
|
||||
} else {
|
||||
file, _ = parser.ParseHCL(src, filename)
|
||||
// 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.
|
||||
//
|
||||
//nolint:nonamedreturns // These names are for documentation purposes, to differentiate two results that have the same type
|
||||
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
|
||||
}
|
||||
|
||||
return file, offset
|
||||
// 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
|
||||
@ -493,7 +565,7 @@ func traversalStr(traversal hcl.Traversal) string {
|
||||
// producing helpful contextual messages in diagnostics. It is not
|
||||
// comprehensive nor intended to be used for other purposes.
|
||||
|
||||
var buf bytes.Buffer
|
||||
var buf strings.Builder
|
||||
for _, step := range traversal {
|
||||
switch tStep := step.(type) {
|
||||
case hcl.TraverseRoot:
|
||||
@ -515,3 +587,41 @@ func traversalStr(traversal hcl.Traversal) string {
|
||||
}
|
||||
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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user