Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 96 additions & 86 deletions internal/command/format/diagnostic.go
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,6 @@ type snippetFormatter struct {
func (f *snippetFormatter) write() {
diag := f.diag
buf := f.buf
color := f.color
if diag.Address != "" {
fmt.Fprintf(buf, " with %s,\n", diag.Address)
}
Expand All @@ -253,109 +252,120 @@ func (f *snippetFormatter) write() {
contextStr = fmt.Sprintf(", in %s", *snippet.Context)
}
fmt.Fprintf(buf, " on %s line %d%s:\n", diag.Range.Filename, diag.Range.Start.Line, contextStr)
f.writeSnippet(snippet, code)

// Split the snippet and render the highlighted section with underlines
start := snippet.HighlightStartOffset
end := snippet.HighlightEndOffset

// Only buggy diagnostics can have an end range before the start, but
// we need to ensure we don't crash here if that happens.
if end < start {
end = start + 1
if end > len(code) {
end = len(code)
}
if diag.DeprecationOriginDescription != "" {
fmt.Fprintf(buf, "\n The deprecation originates from %s\n", diag.DeprecationOriginDescription)
}
}

// If either start or end is out of range for the code buffer then
// we'll cap them at the bounds just to avoid a panic, although
// this would happen only if there's a bug in the code generating
// the snippet objects.
if start < 0 {
start = 0
} else if start > len(code) {
start = len(code)
}
if end < 0 {
end = 0
} else if end > len(code) {
buf.WriteByte('\n')
}

func (f *snippetFormatter) writeSnippet(snippet *viewsjson.DiagnosticSnippet, code string) {
buf := f.buf
color := f.color

// Split the snippet and render the highlighted section with underlines
start := snippet.HighlightStartOffset
end := snippet.HighlightEndOffset

// Only buggy diagnostics can have an end range before the start, but
// we need to ensure we don't crash here if that happens.
if end < start {
end = start + 1
if end > len(code) {
end = len(code)
}
}

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,
)
}
// If either start or end is out of range for the code buffer then
// we'll cap them at the bounds just to avoid a panic, although
// this would happen only if there's a bug in the code generating
// the snippet objects.
if start < 0 {
start = 0
} else if start > len(code) {
start = len(code)
}
if end < 0 {
end = 0
} else if end > len(code) {
end = len(code)
}

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,
)
}

if len(snippet.Values) > 0 || (snippet.FunctionCall != nil && snippet.FunctionCall.Signature != nil) || snippet.TestAssertionExpr != nil {
// 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 := slices.Clone(snippet.Values)
sort.Slice(values, func(i, j int) bool {
return values[i].Traversal < values[j].Traversal
})

fmt.Fprint(buf, color.Color(" [dark_gray]├────────────────[reset]\n"))
if callInfo := snippet.FunctionCall; callInfo != nil && callInfo.Signature != nil {

fmt.Fprintf(buf, color.Color(" [dark_gray]│[reset] while calling [bold]%s[reset]("), callInfo.CalledAs)
for i, param := range callInfo.Signature.Params {
if i > 0 {
buf.WriteString(", ")
}
buf.WriteString(param.Name)
if len(snippet.Values) > 0 || (snippet.FunctionCall != nil && snippet.FunctionCall.Signature != nil) || snippet.TestAssertionExpr != nil {
// 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 := slices.Clone(snippet.Values)
sort.Slice(values, func(i, j int) bool {
return values[i].Traversal < values[j].Traversal
})

fmt.Fprint(buf, color.Color(" [dark_gray]├────────────────[reset]\n"))
if callInfo := snippet.FunctionCall; callInfo != nil && callInfo.Signature != nil {

fmt.Fprintf(buf, color.Color(" [dark_gray]│[reset] while calling [bold]%s[reset]("), callInfo.CalledAs)
for i, param := range callInfo.Signature.Params {
if i > 0 {
buf.WriteString(", ")
}
if param := callInfo.Signature.VariadicParam; param != nil {
if len(callInfo.Signature.Params) > 0 {
buf.WriteString(", ")
}
buf.WriteString(param.Name)
buf.WriteString("...")
buf.WriteString(param.Name)
}
if param := callInfo.Signature.VariadicParam; param != nil {
if len(callInfo.Signature.Params) > 0 {
buf.WriteString(", ")
}
buf.WriteString(")\n")
buf.WriteString(param.Name)
buf.WriteString("...")
}
buf.WriteString(")\n")
}

// always print the values unless in the case of a test assertion, where we only print them if the user has requested verbose output
printValues := snippet.TestAssertionExpr == nil || snippet.TestAssertionExpr.ShowVerbose
// always print the values unless in the case of a test assertion, where we only print them if the user has requested verbose output
printValues := snippet.TestAssertionExpr == nil || snippet.TestAssertionExpr.ShowVerbose

// The diagnostic may also have information about failures from test assertions
// in a `terraform test` run. This is useful for understanding the values that
// were being compared when the assertion failed.
// Also, we'll print a JSON diff of the two values to make it easier to see the
// differences.
if snippet.TestAssertionExpr != nil {
f.printTestDiagOutput(snippet.TestAssertionExpr)
}
// The diagnostic may also have information about failures from test assertions
// in a `terraform test` run. This is useful for understanding the values that
// were being compared when the assertion failed.
// Also, we'll print a JSON diff of the two values to make it easier to see the
// differences.
if snippet.TestAssertionExpr != nil {
f.printTestDiagOutput(snippet.TestAssertionExpr)
}

if printValues {
for _, value := range values {
// if the statement is one line, we'll just print it as is
// otherwise, we have to ensure that each line is indented correctly
// and that the first line has the traversal information
valSlice := strings.Split(value.Statement, "\n")
fmt.Fprintf(buf, color.Color(" [dark_gray]│[reset] [bold]%s[reset] %s\n"),
value.Traversal, valSlice[0])

for _, line := range valSlice[1:] {
fmt.Fprintf(buf, color.Color(" [dark_gray]│[reset] %s\n"), line)
}
if printValues {
for _, value := range values {
// if the statement is one line, we'll just print it as is
// otherwise, we have to ensure that each line is indented correctly
// and that the first line has the traversal information
valSlice := strings.Split(value.Statement, "\n")
fmt.Fprintf(buf, color.Color(" [dark_gray]│[reset] [bold]%s[reset] %s\n"),
value.Traversal, valSlice[0])

for _, line := range valSlice[1:] {
fmt.Fprintf(buf, color.Color(" [dark_gray]│[reset] %s\n"), line)
}
}
}
}

buf.WriteByte('\n')
}

func (f *snippetFormatter) printTestDiagOutput(diag *viewsjson.DiagnosticTestBinaryExpr) {
Expand Down
67 changes: 61 additions & 6 deletions internal/command/format/diagnostic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -405,12 +405,39 @@ func TestDiagnostic(t *testing.T) {
[red]│[reset]
[red]│[reset] Example crash
[red]╵[reset]
`,
},
"warning from deprecation": {
&hcl.Diagnostic{
Severity: hcl.DiagWarning,
Summary: "Deprecation detected",
Detail: "Countermeasures must be taken.",
Subject: &hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{Line: 1, Column: 6, Byte: 5},
End: hcl.Pos{Line: 1, Column: 12, Byte: 11},
},
Extra: &tfdiags.DeprecationOriginDiagnosticExtra{
OriginDescription: "module.foo.bar",
},
},
`[yellow]╷[reset]
[yellow]│[reset] [bold][yellow]Warning: [reset][bold]Deprecation detected[reset]
[yellow]│[reset]
[yellow]│[reset] on test.tf line 1:
[yellow]│[reset] 1: test [underline]source[reset] code
[yellow]│[reset]
[yellow]│[reset] The deprecation originates from module.foo.bar
[yellow]│[reset]
[yellow]│[reset] Countermeasures must be taken.
[yellow]╵[reset]
`,
},
}

sources := map[string][]byte{
"test.tf": []byte(`test source code`),
"test.tf": []byte(`test source code`),
"deprecated.tf": []byte(`source of deprecation`),
}

// This empty Colorize just passes through all of the formatting codes
Expand All @@ -424,8 +451,9 @@ func TestDiagnostic(t *testing.T) {
diag := diags[0]
got := strings.TrimSpace(Diagnostic(diag, sources, colorize, 40))
want := strings.TrimSpace(test.Want)
if got != want {
t.Errorf("wrong result\ngot:\n%s\n\nwant:\n%s\n\n", got, want)

if diff := cmp.Diff(got, want); diff != "" {
t.Errorf("wrong result\ngot:\n%s\n\nwant:\n%s\n\ndiff:\n%s\n\n", got, want, diff)
}
})
}
Expand Down Expand Up @@ -713,12 +741,39 @@ Error: Bad bad bad
1: test source code

Whatever shall we do?
`,
},

"warning from deprecation": {
&hcl.Diagnostic{
Severity: hcl.DiagWarning,
Summary: "Deprecation detected",
Detail: "Countermeasures must be taken.",
Subject: &hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{Line: 1, Column: 6, Byte: 5},
End: hcl.Pos{Line: 1, Column: 12, Byte: 11},
},
Extra: &tfdiags.DeprecationOriginDiagnosticExtra{
OriginDescription: "module.foo.bar",
},
},
`
Warning: Deprecation detected

on test.tf line 1:
1: test source code

The deprecation originates from module.foo.bar

Countermeasures must be taken.
`,
},
}

sources := map[string][]byte{
"test.tf": []byte(`test source code`),
"test.tf": []byte(`test source code`),
"deprecated.tf": []byte(`source of deprecation`),
}

for name, test := range tests {
Expand All @@ -728,8 +783,8 @@ Whatever shall we do?
diag := diags[0]
got := strings.TrimSpace(DiagnosticPlain(diag, sources, 40))
want := strings.TrimSpace(test.Want)
if got != want {
t.Errorf("wrong result\ngot:\n%s\n\nwant:\n%s\n\n", got, want)
if diff := cmp.Diff(got, want); diff != "" {
t.Errorf("wrong result\ngot:\n%s\n\nwant:\n%s\n\n,diff:\n%s\n\n", got, want, diff)
}
})
}
Expand Down
Loading