@ -1,9 +1,14 @@
package format
import (
"bufio"
"bytes"
"fmt"
"strings"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hcled"
"github.com/hashicorp/hcl2/hclparse"
"github.com/hashicorp/terraform/tfdiags"
"github.com/mitchellh/colorstring"
wordwrap "github.com/mitchellh/go-wordwrap"
@ -16,7 +21,7 @@ import (
// at all. Although the long-form text parts of the message are wrapped,
// not all aspects of the message are guaranteed to fit within the specified
// terminal width.
func Diagnostic ( diag tfdiags . Diagnostic , color * colorstring . Colorize , width int ) string {
func Diagnostic ( diag tfdiags . Diagnostic , sources map [ string ] [ ] byte , color * colorstring . Colorize , width int ) string {
if diag == nil {
// No good reason to pass a nil diagnostic in here...
return ""
@ -41,17 +46,74 @@ func Diagnostic(diag tfdiags.Diagnostic, color *colorstring.Colorize, width int)
// 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 )
if sourceRefs . Subject != nil {
fmt . Fprintf ( & buf , color . Color ( "[bold]%s[reset] at %s\n\n" ) , desc . Summary , sourceRefs . Subject . StartString ( ) )
} else {
fmt . Fprintf ( & buf , color . Color ( "[bold]%s[reset]\n\n" ) , desc . Summary )
}
// 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 )
// TODO: also print out the relevant snippet of config source with the
// relevant section highlighted, so the user doesn't need to manually
// correlate back to config. Before we can do this, the HCL2 parser
// needs to be more deeply integrated so that we can use it to obtain
// the parsed source code and AST.
// We can't illustrate an empty range, so we'll turn such ranges into
// single-character ranges, which might not be totally valid (may point
// off the end of a line, or off the end of the file) but are good
// enough for the bounds checks we do below.
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 {
// 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\n" , highlightRange . Filename , highlightRange . Start . Line )
} else {
contextStr := sourceCodeContextStr ( src , highlightRange )
if contextStr != "" {
contextStr = ", in " + contextStr
}
fmt . Fprintf ( & buf , " on %s line %d%s:\n" , highlightRange . Filename , highlightRange . Start . Line , contextStr )
sc := hcl . NewRangeScanner ( src , highlightRange . Filename , bufio . ScanLines )
for sc . Scan ( ) {
lineRange := sc . Range ( )
if ! lineRange . Overlaps ( snippetRange ) {
continue
}
beforeRange , highlightedRange , afterRange := lineRange . PartitionAround ( highlightRange )
if highlightedRange . Empty ( ) {
fmt . Fprintf ( & buf , "%4d: %s\n" , lineRange . Start . Line , sc . Bytes ( ) )
} else {
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 ,
)
}
}
buf . WriteByte ( '\n' )
}
}
if desc . Detail != "" {
detail := desc . Detail
@ -63,3 +125,33 @@ func Diagnostic(diag tfdiags.Diagnostic, color *colorstring.Colorize, width int)
return buf . String ( )
}
// sourceCodeContextStr attempts to find a user-friendly description of
// the location of the given range in the given source code.
//
// An empty string is returned if no suitable description is available, e.g.
// because the source is invalid, or because the offset is not inside any sort
// of identifiable container.
func sourceCodeContextStr ( src [ ] byte , rng hcl . Range ) string {
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 ""
}
return hcled . ContextString ( file , offset )
}