diff --git a/internal/command/format/diagnostic.go b/internal/command/format/diagnostic.go index 5915f26bdf..73a11d76d3 100644 --- a/internal/command/format/diagnostic.go +++ b/internal/command/format/diagnostic.go @@ -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) } @@ -253,109 +252,116 @@ 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 + buf.WriteByte('\n') +} - // 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) - } - } +func (f *snippetFormatter) writeSnippet(snippet *viewsjson.DiagnosticSnippet, code string) { + buf := f.buf + color := f.color - // 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) { + // 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) + // 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) + } - // 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, - ) + 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 param := callInfo.Signature.VariadicParam; param != nil { + if len(callInfo.Signature.Params) > 0 { + buf.WriteString(", ") + } + buf.WriteString(param.Name) + buf.WriteString("...") + } + buf.WriteString(")\n") } - 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 - }) + // 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 - fmt.Fprint(buf, color.Color(" [dark_gray]├────────────────[reset]\n")) - if callInfo := snippet.FunctionCall; callInfo != nil && callInfo.Signature != nil { + // 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) + } - 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 param := callInfo.Signature.VariadicParam; param != nil { - if len(callInfo.Signature.Params) > 0 { - buf.WriteString(", ") - } - buf.WriteString(param.Name) - buf.WriteString("...") - } - buf.WriteString(")\n") - } + 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]) - // 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) - } - - 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) - } + 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) {