cli: Add comprehensive JSON diagnostic structure

This commit adds a comprehensive JSON format for diagnostics, which
ensures that all current diagnostic output can be semantically
represented in a machine-readable format. The diagnostic formatter
interface remains unchanged, but it first transforms its input via the
JSON format to ensure that there is only one code path for creating the
diagnostic data.

The JSON diagnostic renderer extracts the non-presentational logic from
the format package, and returns a structure which can either be
marshaled into JSON or rendered as text. The resulting text diagnostic
output is unchanged for all cases covered by unit tests and my own
manual testing.

Included in this commit are a number of golden reference files for the
marshaled JSON output of a diagnostic. This format should change rarely
if at all, and these are in place to ensure that any changes to the
format are intentional and considered.
This commit is contained in:
Alisdair McDiarmid 2021-03-11 12:08:47 -05:00 committed by Alisdair McDiarmid
parent dc7f2b7314
commit d9d88b9243
14 changed files with 1458 additions and 268 deletions

View File

@ -7,13 +7,11 @@ import (
"sort"
"strings"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hcled"
"github.com/hashicorp/hcl/v2/hclparse"
viewsjson "github.com/hashicorp/terraform/command/views/json"
"github.com/hashicorp/terraform/tfdiags"
"github.com/mitchellh/colorstring"
wordwrap "github.com/mitchellh/go-wordwrap"
"github.com/zclconf/go-cty/cty"
)
var disabledColorize = &colorstring.Colorize{
@ -29,6 +27,10 @@ var disabledColorize = &colorstring.Colorize{
// not all aspects of the message are guaranteed to fit within the specified
// terminal width.
func Diagnostic(diag tfdiags.Diagnostic, sources map[string][]byte, color *colorstring.Colorize, width int) string {
return DiagnosticFromJSON(viewsjson.NewDiagnostic(diag, sources), color, width)
}
func DiagnosticFromJSON(diag *viewsjson.Diagnostic, color *colorstring.Colorize, width int) string {
if diag == nil {
// No good reason to pass a nil diagnostic in here...
return ""
@ -48,14 +50,14 @@ func Diagnostic(diag tfdiags.Diagnostic, sources map[string][]byte, color *color
var leftRuleLine, leftRuleStart, leftRuleEnd string
var leftRuleWidth int // in visual character cells
switch diag.Severity() {
case tfdiags.Error:
switch diag.Severity {
case viewsjson.DiagnosticSeverityError:
buf.WriteString(color.Color("[bold][red]Error: [reset]"))
leftRuleLine = color.Color("[red]│[reset] ")
leftRuleStart = color.Color("[red]╷[reset]")
leftRuleEnd = color.Color("[red]╵[reset]")
leftRuleWidth = 2
case tfdiags.Warning:
case viewsjson.DiagnosticSeverityWarning:
buf.WriteString(color.Color("[bold][yellow]Warning: [reset]"))
leftRuleLine = color.Color("[yellow]│[reset] ")
leftRuleStart = color.Color("[yellow]╷[reset]")
@ -67,22 +69,17 @@ func Diagnostic(diag tfdiags.Diagnostic, sources map[string][]byte, color *color
buf.WriteString(color.Color("\n[reset]"))
}
desc := diag.Description()
sourceRefs := diag.Source()
// We don't wrap the summary, since we expect it to be terse, and since
// this is where we put the text of a native Go error it may not always
// be pure text that lends itself well to word-wrapping.
fmt.Fprintf(&buf, color.Color("[bold]%s[reset]\n\n"), desc.Summary)
fmt.Fprintf(&buf, color.Color("[bold]%s[reset]\n\n"), diag.Summary)
if sourceRefs.Subject != nil {
buf = appendSourceSnippets(buf, diag, sources, color)
}
appendSourceSnippets(&buf, diag, color)
if desc.Detail != "" {
if diag.Detail != "" {
paraWidth := width - leftRuleWidth - 1 // leave room for the left rule
if paraWidth > 0 {
lines := strings.Split(desc.Detail, "\n")
lines := strings.Split(diag.Detail, "\n")
for _, line := range lines {
if !strings.HasPrefix(line, " ") {
line = wordwrap.WrapString(line, uint(paraWidth))
@ -90,7 +87,7 @@ func Diagnostic(diag tfdiags.Diagnostic, sources map[string][]byte, color *color
fmt.Fprintf(&buf, "%s\n", line)
}
} else {
fmt.Fprintf(&buf, "%s\n", desc.Detail)
fmt.Fprintf(&buf, "%s\n", diag.Detail)
}
}
@ -126,6 +123,10 @@ func Diagnostic(diag tfdiags.Diagnostic, sources map[string][]byte, color *color
// It is intended for use in automation and other contexts in which diagnostic
// messages are parsed from the Terraform output.
func DiagnosticPlain(diag tfdiags.Diagnostic, sources map[string][]byte, width int) string {
return DiagnosticPlainFromJSON(viewsjson.NewDiagnostic(diag, sources), width)
}
func DiagnosticPlainFromJSON(diag *viewsjson.Diagnostic, width int) string {
if diag == nil {
// No good reason to pass a nil diagnostic in here...
return ""
@ -133,30 +134,25 @@ func DiagnosticPlain(diag tfdiags.Diagnostic, sources map[string][]byte, width i
var buf bytes.Buffer
switch diag.Severity() {
case tfdiags.Error:
switch diag.Severity {
case viewsjson.DiagnosticSeverityError:
buf.WriteString("\nError: ")
case tfdiags.Warning:
case viewsjson.DiagnosticSeverityWarning:
buf.WriteString("\nWarning: ")
default:
buf.WriteString("\n")
}
desc := diag.Description()
sourceRefs := diag.Source()
// We don't wrap the summary, since we expect it to be terse, and since
// this is where we put the text of a native Go error it may not always
// be pure text that lends itself well to word-wrapping.
fmt.Fprintf(&buf, "%s\n\n", desc.Summary)
fmt.Fprintf(&buf, "%s\n\n", diag.Summary)
if sourceRefs.Subject != nil {
buf = appendSourceSnippets(buf, diag, sources, disabledColorize)
}
appendSourceSnippets(&buf, diag, disabledColorize)
if desc.Detail != "" {
if diag.Detail != "" {
if width > 1 {
lines := strings.Split(desc.Detail, "\n")
lines := strings.Split(diag.Detail, "\n")
for _, line := range lines {
if !strings.HasPrefix(line, " ") {
line = wordwrap.WrapString(line, uint(width-1))
@ -164,7 +160,7 @@ func DiagnosticPlain(diag tfdiags.Diagnostic, sources map[string][]byte, width i
fmt.Fprintf(&buf, "%s\n", line)
}
} else {
fmt.Fprintf(&buf, "%s\n", desc.Detail)
fmt.Fprintf(&buf, "%s\n", diag.Detail)
}
}
@ -216,259 +212,61 @@ func DiagnosticWarningsCompact(diags tfdiags.Diagnostics, color *colorstring.Col
return b.String()
}
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
var diags hcl.Diagnostics
if strings.HasSuffix(filename, ".json") {
file, diags = parser.ParseJSON(src, filename)
} else {
file, diags = parser.ParseHCL(src, filename)
}
if diags.HasErrors() {
return file, offset
func appendSourceSnippets(buf *bytes.Buffer, diag *viewsjson.Diagnostic, color *colorstring.Colorize) {
if diag.Range == nil {
return
}
return file, offset
}
// 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 bytes.Buffer
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()
}
// 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.ContainsMarked() {
return "(sensitive value)"
}
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()
}
}
func appendSourceSnippets(buf bytes.Buffer, diag tfdiags.Diagnostic, sources map[string][]byte, color *colorstring.Colorize) bytes.Buffer {
sourceRefs := diag.Source()
// 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()
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)
if snippetRange.Empty() {
snippetRange.End.Byte++
snippetRange.End.Column++
}
if highlightRange.Empty() {
highlightRange.End.Byte++
highlightRange.End.Column++
}
var src []byte
if sources != nil {
src = sources[snippetRange.Filename]
}
if src == nil {
if diag.Snippet == nil {
// This should generally not happen, as long as sources are always
// loaded through the main loader. We may load things in other
// ways in weird cases, so we'll tolerate it at the expense of
// a not-so-helpful error message.
fmt.Fprintf(&buf, " on %s line %d:\n (source code not available)\n", highlightRange.Filename, highlightRange.Start.Line)
fmt.Fprintf(buf, " on %s line %d:\n (source code not available)\n", diag.Range.Filename, diag.Range.Start.Line)
} else {
file, offset := parseRange(src, highlightRange)
snippet := diag.Snippet
code := snippet.Code
headerRange := highlightRange
var contextStr string
if snippet.Context != nil {
contextStr = fmt.Sprintf(", in %s", *snippet.Context)
}
fmt.Fprintf(buf, " on %s line %d%s:\n", diag.Range.Filename, diag.Range.Start.Line, contextStr)
contextStr := hcled.ContextString(file, offset-1)
if contextStr != "" {
contextStr = ", in " + contextStr
// Split the snippet and render the highlighted section with underlines
start := snippet.HighlightStartOffset
end := snippet.HighlightEndOffset
before, highlight, after := code[0:start], code[start:end], code[end:]
code = fmt.Sprintf(color.Color("%s[underline]%s[reset]%s"), before, highlight, after)
// Split the snippet into lines and render one at a time
lines := strings.Split(code, "\n")
for i, line := range lines {
fmt.Fprintf(
buf, "%4d: %s\n",
snippet.StartLine+i,
line,
)
}
fmt.Fprintf(&buf, " on %s line %d%s:\n", headerRange.Filename, headerRange.Start.Line, contextStr)
if len(snippet.Values) > 0 {
// The diagnostic may also have information about the dynamic
// values of relevant variables at the point of evaluation.
// 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.
values := make([]viewsjson.DiagnosticExpressionValue, len(snippet.Values))
copy(values, snippet.Values)
sort.Slice(values, func(i, j int) bool {
return values[i].Traversal < values[j].Traversal
})
// Config snippet rendering
sc := hcl.NewRangeScanner(src, highlightRange.Filename, bufio.ScanLines)
for sc.Scan() {
lineRange := sc.Range()
if !lineRange.Overlaps(snippetRange) {
continue
fmt.Fprint(buf, color.Color(" [dark_gray]├────────────────[reset]\n"))
for _, value := range values {
fmt.Fprintf(buf, color.Color(" [dark_gray]│[reset] [bold]%s[reset] %s\n"), value.Traversal, value.Statement)
}
if !lineRange.Overlap(highlightRange).Empty() {
beforeRange, highlightedRange, afterRange := lineRange.PartitionAround(highlightRange)
before := beforeRange.SliceBytes(src)
highlighted := highlightedRange.SliceBytes(src)
after := afterRange.SliceBytes(src)
fmt.Fprintf(
&buf, color.Color("%4d: %s[underline]%s[reset]%s\n"),
lineRange.Start.Line,
before, highlighted, after,
)
} else {
fmt.Fprintf(
&buf, "%4d: %s\n",
lineRange.Start.Line,
lineRange.SliceBytes(src),
)
}
}
}
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()
stmts := make([]string, 0, len(vars))
seen := make(map[string]struct{}, len(vars))
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
}
switch {
case val.IsMarked():
// We won't say anything at all about sensitive values,
// because we might give away something that was
// sensitive about them.
stmts = append(stmts, fmt.Sprintf(color.Color("[bold]%s[reset] has a sensitive value"), traversalStr))
case !val.IsKnown():
if ty := val.Type(); ty != cty.DynamicPseudoType {
stmts = append(stmts, fmt.Sprintf(color.Color("[bold]%s[reset] is a %s, known only after apply"), traversalStr, ty.FriendlyName()))
} else {
stmts = append(stmts, fmt.Sprintf(color.Color("[bold]%s[reset] will be known only after apply"), traversalStr))
}
case val.IsNull():
stmts = append(stmts, fmt.Sprintf(color.Color("[bold]%s[reset] is null"), traversalStr))
default:
stmts = append(stmts, fmt.Sprintf(color.Color("[bold]%s[reset] is %s"), traversalStr, compactValueStr(val)))
}
seen[traversalStr] = struct{}{}
}
}
sort.Strings(stmts) // FIXME: Should maybe use a traversal-aware sort that can sort numeric indexes properly?
if len(stmts) > 0 {
fmt.Fprint(&buf, color.Color(" [dark_gray]├────────────────[reset]\n"))
}
for _, stmt := range stmts {
fmt.Fprintf(&buf, color.Color(" [dark_gray]│[reset] %s\n"), stmt)
}
}
buf.WriteByte('\n')
return buf
}

View File

@ -0,0 +1,401 @@
package json
import (
"bufio"
"bytes"
"fmt"
"sort"
"strings"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hcled"
"github.com/hashicorp/hcl/v2/hclparse"
"github.com/hashicorp/terraform/tfdiags"
"github.com/zclconf/go-cty/cty"
)
// 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"`
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 Terraform 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"`
}
// 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"`
}
// NewDiagnostic takes a tfdiags.Diagnostic and a map of configuration sources,
// and returns a Diagnostic struct.
func NewDiagnostic(diag tfdiags.Diagnostic, sources map[string][]byte) *Diagnostic {
var sev string
switch diag.Severity() {
case tfdiags.Error:
sev = DiagnosticSeverityError
case tfdiags.Warning:
sev = DiagnosticSeverityWarning
default:
sev = DiagnosticSeverityUnknown
}
desc := diag.Description()
diagnostic := &Diagnostic{
Severity: sev,
Summary: desc.Summary,
Detail: desc.Detail,
}
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()
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 {
src = sources[highlightRange.Filename]
}
// 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)
if start > len(codeStr) {
start = len(codeStr)
}
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))
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.IsMarked():
// We won't say anything at all about sensitive values,
// because we might give away something that was
// sensitive about them.
value.Statement = "has a sensitive value"
case !val.IsKnown():
if ty := val.Type(); ty != cty.DynamicPseudoType {
value.Statement = fmt.Sprintf("is a %s, known only after apply", ty.FriendlyName())
} else {
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
}
}
}
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)
}
return file, offset
}
// 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.ContainsMarked() {
return "(sensitive value)"
}
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 bytes.Buffer
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()
}

View File

@ -0,0 +1,669 @@
package json
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hcltest"
"github.com/hashicorp/terraform/tfdiags"
"github.com/zclconf/go-cty/cty"
)
func TestNewDiagnostic(t *testing.T) {
// Common HCL for diags with source ranges. This does not have any real
// semantic errors, but we can synthesize fake HCL errors which will
// exercise the diagnostic rendering code using this
sources := map[string][]byte{
"test.tf": []byte(`resource "test_resource" "test" {
foo = var.boop["hello!"]
bar = {
baz = maybe
}
}
`),
"short.tf": []byte("bad source code"),
"values.tf": []byte(`[
var.a,
var.b,
var.c,
var.d,
var.e,
var.f,
var.g,
var.h,
var.i,
var.j,
var.k,
]
`),
}
testCases := map[string]struct {
diag interface{} // allow various kinds of diags
want *Diagnostic
}{
"sourceless warning": {
tfdiags.Sourceless(
tfdiags.Warning,
"Oh no",
"Something is broken",
),
&Diagnostic{
Severity: "warning",
Summary: "Oh no",
Detail: "Something is broken",
},
},
"error with source code unavailable": {
&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Bad news",
Detail: "It went wrong",
Subject: &hcl.Range{
Filename: "modules/oops/missing.tf",
Start: hcl.Pos{Line: 1, Column: 6, Byte: 5},
End: hcl.Pos{Line: 2, Column: 12, Byte: 33},
},
},
&Diagnostic{
Severity: "error",
Summary: "Bad news",
Detail: "It went wrong",
Range: &DiagnosticRange{
Filename: "modules/oops/missing.tf",
Start: Pos{
Line: 1,
Column: 6,
Byte: 5,
},
End: Pos{
Line: 2,
Column: 12,
Byte: 33,
},
},
},
},
"error with source code subject": {
&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Tiny explosion",
Detail: "Unexpected detonation while parsing",
Subject: &hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{Line: 1, Column: 10, Byte: 9},
End: hcl.Pos{Line: 1, Column: 25, Byte: 24},
},
},
&Diagnostic{
Severity: "error",
Summary: "Tiny explosion",
Detail: "Unexpected detonation while parsing",
Range: &DiagnosticRange{
Filename: "test.tf",
Start: Pos{
Line: 1,
Column: 10,
Byte: 9,
},
End: Pos{
Line: 1,
Column: 25,
Byte: 24,
},
},
Snippet: &DiagnosticSnippet{
Context: strPtr(`resource "test_resource" "test"`),
Code: `resource "test_resource" "test" {`,
StartLine: 1,
HighlightStartOffset: 9,
HighlightEndOffset: 24,
Values: []DiagnosticExpressionValue{},
},
},
},
"error with source code subject but no context": {
&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Nonsense input",
Detail: "What you wrote makes no sense",
Subject: &hcl.Range{
Filename: "short.tf",
Start: hcl.Pos{Line: 1, Column: 5, Byte: 4},
End: hcl.Pos{Line: 1, Column: 10, Byte: 9},
},
},
&Diagnostic{
Severity: "error",
Summary: "Nonsense input",
Detail: "What you wrote makes no sense",
Range: &DiagnosticRange{
Filename: "short.tf",
Start: Pos{
Line: 1,
Column: 5,
Byte: 4,
},
End: Pos{
Line: 1,
Column: 10,
Byte: 9,
},
},
Snippet: &DiagnosticSnippet{
Context: nil,
Code: (`bad source code`),
StartLine: (1),
HighlightStartOffset: (4),
HighlightEndOffset: (9),
Values: []DiagnosticExpressionValue{},
},
},
},
"error with multi-line snippet": {
&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "In this house we respect booleans",
Detail: "True or false, there is no maybe",
Subject: &hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{Line: 4, Column: 11, Byte: 81},
End: hcl.Pos{Line: 4, Column: 16, Byte: 86},
},
Context: &hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{Line: 3, Column: 3, Byte: 63},
End: hcl.Pos{Line: 5, Column: 4, Byte: 90},
},
},
&Diagnostic{
Severity: "error",
Summary: "In this house we respect booleans",
Detail: "True or false, there is no maybe",
Range: &DiagnosticRange{
Filename: "test.tf",
Start: Pos{
Line: 4,
Column: 11,
Byte: 81,
},
End: Pos{
Line: 4,
Column: 16,
Byte: 86,
},
},
Snippet: &DiagnosticSnippet{
Context: strPtr(`resource "test_resource" "test"`),
Code: " bar = {\n baz = maybe\n }",
StartLine: 3,
HighlightStartOffset: 20,
HighlightEndOffset: 25,
Values: []DiagnosticExpressionValue{},
},
},
},
"error with empty highlight range at end of source code": {
&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "You forgot something",
Detail: "Please finish your thought",
Subject: &hcl.Range{
Filename: "short.tf",
Start: hcl.Pos{Line: 1, Column: 16, Byte: 15},
End: hcl.Pos{Line: 1, Column: 16, Byte: 15},
},
},
&Diagnostic{
Severity: "error",
Summary: "You forgot something",
Detail: "Please finish your thought",
Range: &DiagnosticRange{
Filename: "short.tf",
Start: Pos{
Line: 1,
Column: 16,
Byte: 15,
},
End: Pos{
Line: 1,
Column: 17,
Byte: 16,
},
},
Snippet: &DiagnosticSnippet{
Code: ("bad source code"),
StartLine: (1),
HighlightStartOffset: (15),
HighlightEndOffset: (15),
Values: []DiagnosticExpressionValue{},
},
},
},
"error with source code subject and known expression": {
&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Wrong noises",
Detail: "Biological sounds are not allowed",
Subject: &hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{Line: 2, Column: 9, Byte: 42},
End: hcl.Pos{Line: 2, Column: 26, Byte: 59},
},
Expression: hcltest.MockExprTraversal(hcl.Traversal{
hcl.TraverseRoot{Name: "var"},
hcl.TraverseAttr{Name: "boop"},
hcl.TraverseIndex{Key: cty.StringVal("hello!")},
}),
EvalContext: &hcl.EvalContext{
Variables: map[string]cty.Value{
"var": cty.ObjectVal(map[string]cty.Value{
"boop": cty.MapVal(map[string]cty.Value{
"hello!": cty.StringVal("bleurgh"),
}),
}),
},
},
},
&Diagnostic{
Severity: "error",
Summary: "Wrong noises",
Detail: "Biological sounds are not allowed",
Range: &DiagnosticRange{
Filename: "test.tf",
Start: Pos{
Line: 2,
Column: 9,
Byte: 42,
},
End: Pos{
Line: 2,
Column: 26,
Byte: 59,
},
},
Snippet: &DiagnosticSnippet{
Context: strPtr(`resource "test_resource" "test"`),
Code: (` foo = var.boop["hello!"]`),
StartLine: (2),
HighlightStartOffset: (8),
HighlightEndOffset: (25),
Values: []DiagnosticExpressionValue{
{
Traversal: `var.boop["hello!"]`,
Statement: `is "bleurgh"`,
},
},
},
},
},
"error with source code subject and expression referring to sensitive value": {
&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Wrong noises",
Detail: "Biological sounds are not allowed",
Subject: &hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{Line: 2, Column: 9, Byte: 42},
End: hcl.Pos{Line: 2, Column: 26, Byte: 59},
},
Expression: hcltest.MockExprTraversal(hcl.Traversal{
hcl.TraverseRoot{Name: "var"},
hcl.TraverseAttr{Name: "boop"},
hcl.TraverseIndex{Key: cty.StringVal("hello!")},
}),
EvalContext: &hcl.EvalContext{
Variables: map[string]cty.Value{
"var": cty.ObjectVal(map[string]cty.Value{
"boop": cty.MapVal(map[string]cty.Value{
"hello!": cty.StringVal("bleurgh").Mark("sensitive"),
}),
}),
},
},
},
&Diagnostic{
Severity: "error",
Summary: "Wrong noises",
Detail: "Biological sounds are not allowed",
Range: &DiagnosticRange{
Filename: "test.tf",
Start: Pos{
Line: 2,
Column: 9,
Byte: 42,
},
End: Pos{
Line: 2,
Column: 26,
Byte: 59,
},
},
Snippet: &DiagnosticSnippet{
Context: strPtr(`resource "test_resource" "test"`),
Code: (` foo = var.boop["hello!"]`),
StartLine: (2),
HighlightStartOffset: (8),
HighlightEndOffset: (25),
Values: []DiagnosticExpressionValue{
{
Traversal: `var.boop["hello!"]`,
Statement: `has a sensitive value`,
},
},
},
},
},
"error with source code subject and unknown string expression": {
&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Wrong noises",
Detail: "Biological sounds are not allowed",
Subject: &hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{Line: 2, Column: 9, Byte: 42},
End: hcl.Pos{Line: 2, Column: 26, Byte: 59},
},
Expression: hcltest.MockExprTraversal(hcl.Traversal{
hcl.TraverseRoot{Name: "var"},
hcl.TraverseAttr{Name: "boop"},
hcl.TraverseIndex{Key: cty.StringVal("hello!")},
}),
EvalContext: &hcl.EvalContext{
Variables: map[string]cty.Value{
"var": cty.ObjectVal(map[string]cty.Value{
"boop": cty.MapVal(map[string]cty.Value{
"hello!": cty.UnknownVal(cty.String),
}),
}),
},
},
},
&Diagnostic{
Severity: "error",
Summary: "Wrong noises",
Detail: "Biological sounds are not allowed",
Range: &DiagnosticRange{
Filename: "test.tf",
Start: Pos{
Line: 2,
Column: 9,
Byte: 42,
},
End: Pos{
Line: 2,
Column: 26,
Byte: 59,
},
},
Snippet: &DiagnosticSnippet{
Context: strPtr(`resource "test_resource" "test"`),
Code: (` foo = var.boop["hello!"]`),
StartLine: (2),
HighlightStartOffset: (8),
HighlightEndOffset: (25),
Values: []DiagnosticExpressionValue{
{
Traversal: `var.boop["hello!"]`,
Statement: `is a string, known only after apply`,
},
},
},
},
},
"error with source code subject and unknown expression of unknown type": {
&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Wrong noises",
Detail: "Biological sounds are not allowed",
Subject: &hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{Line: 2, Column: 9, Byte: 42},
End: hcl.Pos{Line: 2, Column: 26, Byte: 59},
},
Expression: hcltest.MockExprTraversal(hcl.Traversal{
hcl.TraverseRoot{Name: "var"},
hcl.TraverseAttr{Name: "boop"},
hcl.TraverseIndex{Key: cty.StringVal("hello!")},
}),
EvalContext: &hcl.EvalContext{
Variables: map[string]cty.Value{
"var": cty.ObjectVal(map[string]cty.Value{
"boop": cty.MapVal(map[string]cty.Value{
"hello!": cty.UnknownVal(cty.DynamicPseudoType),
}),
}),
},
},
},
&Diagnostic{
Severity: "error",
Summary: "Wrong noises",
Detail: "Biological sounds are not allowed",
Range: &DiagnosticRange{
Filename: "test.tf",
Start: Pos{
Line: 2,
Column: 9,
Byte: 42,
},
End: Pos{
Line: 2,
Column: 26,
Byte: 59,
},
},
Snippet: &DiagnosticSnippet{
Context: strPtr(`resource "test_resource" "test"`),
Code: (` foo = var.boop["hello!"]`),
StartLine: (2),
HighlightStartOffset: (8),
HighlightEndOffset: (25),
Values: []DiagnosticExpressionValue{
{
Traversal: `var.boop["hello!"]`,
Statement: `will be known only after apply`,
},
},
},
},
},
"error with source code subject with multiple expression values": {
&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Catastrophic failure",
Detail: "Basically, everything went wrong",
Subject: &hcl.Range{
Filename: "values.tf",
Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
End: hcl.Pos{Line: 13, Column: 2, Byte: 102},
},
Expression: hcltest.MockExprList([]hcl.Expression{
hcltest.MockExprTraversalSrc("var.a"),
hcltest.MockExprTraversalSrc("var.b"),
hcltest.MockExprTraversalSrc("var.c"),
hcltest.MockExprTraversalSrc("var.d"),
hcltest.MockExprTraversalSrc("var.e"),
hcltest.MockExprTraversalSrc("var.f"),
hcltest.MockExprTraversalSrc("var.g"),
hcltest.MockExprTraversalSrc("var.h"),
hcltest.MockExprTraversalSrc("var.i"),
hcltest.MockExprTraversalSrc("var.j"),
hcltest.MockExprTraversalSrc("var.k"),
}),
EvalContext: &hcl.EvalContext{
Variables: map[string]cty.Value{
"var": cty.ObjectVal(map[string]cty.Value{
"a": cty.True,
"b": cty.NumberFloatVal(123.45),
"c": cty.NullVal(cty.String),
"d": cty.StringVal("secret").Mark("sensitive"),
"e": cty.False,
"f": cty.ListValEmpty(cty.String),
"g": cty.MapVal(map[string]cty.Value{
"boop": cty.StringVal("beep"),
}),
"h": cty.ListVal([]cty.Value{
cty.StringVal("boop"),
cty.StringVal("beep"),
cty.StringVal("blorp"),
}),
"i": cty.EmptyObjectVal,
"j": cty.ObjectVal(map[string]cty.Value{
"foo": cty.StringVal("bar"),
}),
"k": cty.ObjectVal(map[string]cty.Value{
"a": cty.True,
"b": cty.False,
}),
}),
},
},
},
&Diagnostic{
Severity: "error",
Summary: "Catastrophic failure",
Detail: "Basically, everything went wrong",
Range: &DiagnosticRange{
Filename: "values.tf",
Start: Pos{
Line: 1,
Column: 1,
Byte: 0,
},
End: Pos{
Line: 13,
Column: 2,
Byte: 102,
},
},
Snippet: &DiagnosticSnippet{
Code: `[
var.a,
var.b,
var.c,
var.d,
var.e,
var.f,
var.g,
var.h,
var.i,
var.j,
var.k,
]`,
StartLine: (1),
HighlightStartOffset: (0),
HighlightEndOffset: (102),
Values: []DiagnosticExpressionValue{
{
Traversal: `var.a`,
Statement: `is true`,
},
{
Traversal: `var.b`,
Statement: `is 123.45`,
},
{
Traversal: `var.c`,
Statement: `is null`,
},
{
Traversal: `var.d`,
Statement: `has a sensitive value`,
},
{
Traversal: `var.e`,
Statement: `is false`,
},
{
Traversal: `var.f`,
Statement: `is empty list of string`,
},
{
Traversal: `var.g`,
Statement: `is map of string with 1 element`,
},
{
Traversal: `var.h`,
Statement: `is list of string with 3 elements`,
},
{
Traversal: `var.i`,
Statement: `is object with no attributes`,
},
{
Traversal: `var.j`,
Statement: `is object with 1 attribute "foo"`,
},
{
Traversal: `var.k`,
Statement: `is object with 2 attributes`,
},
},
},
},
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
// Convert the diag into a tfdiags.Diagnostic
var diags tfdiags.Diagnostics
diags = diags.Append(tc.diag)
got := NewDiagnostic(diags[0], sources)
if !cmp.Equal(tc.want, got) {
t.Fatalf("wrong result\n:%s", cmp.Diff(tc.want, got))
}
})
t.Run(fmt.Sprintf("golden test for %s", name), func(t *testing.T) {
// Convert the diag into a tfdiags.Diagnostic
var diags tfdiags.Diagnostics
diags = diags.Append(tc.diag)
got := NewDiagnostic(diags[0], sources)
// Render the diagnostic to indented JSON
gotBytes, err := json.MarshalIndent(got, "", " ")
if err != nil {
t.Fatal(err)
}
// Compare against the golden reference
filename := path.Join(
"testdata",
"diagnostic",
fmt.Sprintf("%s.json", strings.ReplaceAll(name, " ", "-")),
)
wantFile, err := os.Open(filename)
if err != nil {
t.Fatalf("failed to open golden file: %s", err)
}
defer wantFile.Close()
wantBytes, err := ioutil.ReadAll(wantFile)
if err != nil {
t.Fatalf("failed to read output file: %s", err)
}
// Don't care about leading or trailing whitespace
gotString := strings.TrimSpace(string(gotBytes))
wantString := strings.TrimSpace(string(wantBytes))
if !cmp.Equal(wantString, gotString) {
t.Fatalf("wrong result\n:%s", cmp.Diff(wantString, gotString))
}
})
}
}
// Helper function to make constructing literal Diagnostics easier. There
// are fields which are pointer-to-string to ensure that the rendered JSON
// results in `null` for an empty value, rather than `""`.
func strPtr(s string) *string { return &s }

View File

@ -0,0 +1,26 @@
{
"severity": "error",
"summary": "You forgot something",
"detail": "Please finish your thought",
"range": {
"filename": "short.tf",
"start": {
"line": 1,
"column": 16,
"byte": 15
},
"end": {
"line": 1,
"column": 17,
"byte": 16
}
},
"snippet": {
"context": null,
"code": "bad source code",
"start_line": 1,
"highlight_start_offset": 15,
"highlight_end_offset": 15,
"values": []
}
}

View File

@ -0,0 +1,26 @@
{
"severity": "error",
"summary": "In this house we respect booleans",
"detail": "True or false, there is no maybe",
"range": {
"filename": "test.tf",
"start": {
"line": 4,
"column": 11,
"byte": 81
},
"end": {
"line": 4,
"column": 16,
"byte": 86
}
},
"snippet": {
"context": "resource \"test_resource\" \"test\"",
"code": " bar = {\n baz = maybe\n }",
"start_line": 3,
"highlight_start_offset": 20,
"highlight_end_offset": 25,
"values": []
}
}

View File

@ -0,0 +1,31 @@
{
"severity": "error",
"summary": "Wrong noises",
"detail": "Biological sounds are not allowed",
"range": {
"filename": "test.tf",
"start": {
"line": 2,
"column": 9,
"byte": 42
},
"end": {
"line": 2,
"column": 26,
"byte": 59
}
},
"snippet": {
"context": "resource \"test_resource\" \"test\"",
"code": " foo = var.boop[\"hello!\"]",
"start_line": 2,
"highlight_start_offset": 8,
"highlight_end_offset": 25,
"values": [
{
"traversal": "var.boop[\"hello!\"]",
"statement": "has a sensitive value"
}
]
}
}

View File

@ -0,0 +1,31 @@
{
"severity": "error",
"summary": "Wrong noises",
"detail": "Biological sounds are not allowed",
"range": {
"filename": "test.tf",
"start": {
"line": 2,
"column": 9,
"byte": 42
},
"end": {
"line": 2,
"column": 26,
"byte": 59
}
},
"snippet": {
"context": "resource \"test_resource\" \"test\"",
"code": " foo = var.boop[\"hello!\"]",
"start_line": 2,
"highlight_start_offset": 8,
"highlight_end_offset": 25,
"values": [
{
"traversal": "var.boop[\"hello!\"]",
"statement": "is \"bleurgh\""
}
]
}
}

View File

@ -0,0 +1,31 @@
{
"severity": "error",
"summary": "Wrong noises",
"detail": "Biological sounds are not allowed",
"range": {
"filename": "test.tf",
"start": {
"line": 2,
"column": 9,
"byte": 42
},
"end": {
"line": 2,
"column": 26,
"byte": 59
}
},
"snippet": {
"context": "resource \"test_resource\" \"test\"",
"code": " foo = var.boop[\"hello!\"]",
"start_line": 2,
"highlight_start_offset": 8,
"highlight_end_offset": 25,
"values": [
{
"traversal": "var.boop[\"hello!\"]",
"statement": "will be known only after apply"
}
]
}
}

View File

@ -0,0 +1,31 @@
{
"severity": "error",
"summary": "Wrong noises",
"detail": "Biological sounds are not allowed",
"range": {
"filename": "test.tf",
"start": {
"line": 2,
"column": 9,
"byte": 42
},
"end": {
"line": 2,
"column": 26,
"byte": 59
}
},
"snippet": {
"context": "resource \"test_resource\" \"test\"",
"code": " foo = var.boop[\"hello!\"]",
"start_line": 2,
"highlight_start_offset": 8,
"highlight_end_offset": 25,
"values": [
{
"traversal": "var.boop[\"hello!\"]",
"statement": "is a string, known only after apply"
}
]
}
}

View File

@ -0,0 +1,26 @@
{
"severity": "error",
"summary": "Nonsense input",
"detail": "What you wrote makes no sense",
"range": {
"filename": "short.tf",
"start": {
"line": 1,
"column": 5,
"byte": 4
},
"end": {
"line": 1,
"column": 10,
"byte": 9
}
},
"snippet": {
"context": null,
"code": "bad source code",
"start_line": 1,
"highlight_start_offset": 4,
"highlight_end_offset": 9,
"values": []
}
}

View File

@ -0,0 +1,71 @@
{
"severity": "error",
"summary": "Catastrophic failure",
"detail": "Basically, everything went wrong",
"range": {
"filename": "values.tf",
"start": {
"line": 1,
"column": 1,
"byte": 0
},
"end": {
"line": 13,
"column": 2,
"byte": 102
}
},
"snippet": {
"context": null,
"code": "[\n var.a,\n var.b,\n var.c,\n var.d,\n var.e,\n var.f,\n var.g,\n var.h,\n var.i,\n var.j,\n var.k,\n]",
"start_line": 1,
"highlight_start_offset": 0,
"highlight_end_offset": 102,
"values": [
{
"traversal": "var.a",
"statement": "is true"
},
{
"traversal": "var.b",
"statement": "is 123.45"
},
{
"traversal": "var.c",
"statement": "is null"
},
{
"traversal": "var.d",
"statement": "has a sensitive value"
},
{
"traversal": "var.e",
"statement": "is false"
},
{
"traversal": "var.f",
"statement": "is empty list of string"
},
{
"traversal": "var.g",
"statement": "is map of string with 1 element"
},
{
"traversal": "var.h",
"statement": "is list of string with 3 elements"
},
{
"traversal": "var.i",
"statement": "is object with no attributes"
},
{
"traversal": "var.j",
"statement": "is object with 1 attribute \"foo\""
},
{
"traversal": "var.k",
"statement": "is object with 2 attributes"
}
]
}
}

View File

@ -0,0 +1,26 @@
{
"severity": "error",
"summary": "Tiny explosion",
"detail": "Unexpected detonation while parsing",
"range": {
"filename": "test.tf",
"start": {
"line": 1,
"column": 10,
"byte": 9
},
"end": {
"line": 1,
"column": 25,
"byte": 24
}
},
"snippet": {
"context": "resource \"test_resource\" \"test\"",
"code": "resource \"test_resource\" \"test\" {",
"start_line": 1,
"highlight_start_offset": 9,
"highlight_end_offset": 24,
"values": []
}
}

View File

@ -0,0 +1,18 @@
{
"severity": "error",
"summary": "Bad news",
"detail": "It went wrong",
"range": {
"filename": "modules/oops/missing.tf",
"start": {
"line": 1,
"column": 6,
"byte": 5
},
"end": {
"line": 2,
"column": 12,
"byte": 33
}
}
}

View File

@ -0,0 +1,5 @@
{
"severity": "warning",
"summary": "Oh no",
"detail": "Something is broken"
}