11 wordwrap "github.com/mitchellh/go-wordwrap"
12 "github.com/zclconf/go-cty/cty"
15 type diagnosticTextWriter struct {
16 files map[string]*File
22 // NewDiagnosticTextWriter creates a DiagnosticWriter that writes diagnostics
23 // to the given writer as formatted text.
25 // It is designed to produce text appropriate to print in a monospaced font
26 // in a terminal of a particular width, or optionally with no width limit.
28 // The given width may be zero to disable word-wrapping of the detail text
29 // and truncation of source code snippets.
31 // If color is set to true, the output will include VT100 escape sequences to
32 // color-code the severity indicators. It is suggested to turn this off if
33 // the target writer is not a terminal.
34 func NewDiagnosticTextWriter(wr io.Writer, files map[string]*File, width uint, color bool) DiagnosticWriter {
35 return &diagnosticTextWriter{
43 func (w *diagnosticTextWriter) WriteDiagnostic(diag *Diagnostic) error {
45 return errors.New("nil diagnostic")
48 var colorCode, highlightCode, resetCode string
50 switch diag.Severity {
52 colorCode = "\x1b[31m"
54 colorCode = "\x1b[33m"
57 highlightCode = "\x1b[1;4m"
60 var severityStr string
61 switch diag.Severity {
65 severityStr = "Warning"
67 // should never happen
68 severityStr = "???????"
71 fmt.Fprintf(w.wr, "%s%s%s: %s\n\n", colorCode, severityStr, resetCode, diag.Summary)
73 if diag.Subject != nil {
74 snipRange := *diag.Subject
75 highlightRange := snipRange
76 if diag.Context != nil {
77 // Show enough of the source code to include both the subject
78 // and context ranges, which overlap in all reasonable
80 snipRange = RangeOver(snipRange, *diag.Context)
82 // We can't illustrate an empty range, so we'll turn such ranges into
83 // single-character ranges, which might not be totally valid (may point
84 // off the end of a line, or off the end of the file) but are good
85 // enough for the bounds checks we do below.
86 if snipRange.Empty() {
88 snipRange.End.Column++
90 if highlightRange.Empty() {
91 highlightRange.End.Byte++
92 highlightRange.End.Column++
95 file := w.files[diag.Subject.Filename]
96 if file == nil || file.Bytes == nil {
97 fmt.Fprintf(w.wr, " on %s line %d:\n (source code not available)\n\n", diag.Subject.Filename, diag.Subject.Start.Line)
100 var contextLine string
101 if diag.Subject != nil {
102 contextLine = contextString(file, diag.Subject.Start.Byte)
103 if contextLine != "" {
104 contextLine = ", in " + contextLine
108 fmt.Fprintf(w.wr, " on %s line %d%s:\n", diag.Subject.Filename, diag.Subject.Start.Line, contextLine)
111 sc := NewRangeScanner(src, diag.Subject.Filename, bufio.ScanLines)
114 lineRange := sc.Range()
115 if !lineRange.Overlaps(snipRange) {
119 beforeRange, highlightedRange, afterRange := lineRange.PartitionAround(highlightRange)
120 if highlightedRange.Empty() {
121 fmt.Fprintf(w.wr, "%4d: %s\n", lineRange.Start.Line, sc.Bytes())
123 before := beforeRange.SliceBytes(src)
124 highlighted := highlightedRange.SliceBytes(src)
125 after := afterRange.SliceBytes(src)
127 w.wr, "%4d: %s%s%s%s%s\n",
128 lineRange.Start.Line,
130 highlightCode, highlighted, resetCode,
137 w.wr.Write([]byte{'\n'})
140 if diag.Expression != nil && diag.EvalContext != nil {
141 // We will attempt to render the values for any variables
142 // referenced in the given expression as additional context, for
143 // situations where the same expression is evaluated multiple
144 // times in different scopes.
145 expr := diag.Expression
146 ctx := diag.EvalContext
148 vars := expr.Variables()
149 stmts := make([]string, 0, len(vars))
150 seen := make(map[string]struct{}, len(vars))
151 for _, traversal := range vars {
152 val, diags := traversal.TraverseAbs(ctx)
153 if diags.HasErrors() {
154 // Skip anything that generates errors, since we probably
155 // already have the same error in our diagnostics set
160 traversalStr := w.traversalStr(traversal)
161 if _, exists := seen[traversalStr]; exists {
162 continue // don't show duplicates when the same variable is referenced multiple times
166 // Can't say anything about this yet, then.
169 stmts = append(stmts, fmt.Sprintf("%s set to null", traversalStr))
171 stmts = append(stmts, fmt.Sprintf("%s as %s", traversalStr, w.valueStr(val)))
173 seen[traversalStr] = struct{}{}
176 sort.Strings(stmts) // FIXME: Should maybe use a traversal-aware sort that can sort numeric indexes properly?
177 last := len(stmts) - 1
179 for i, stmt := range stmts {
182 w.wr.Write([]byte{'w', 'i', 't', 'h', ' '})
184 w.wr.Write([]byte{' ', ' ', ' ', ' ', ' '})
186 w.wr.Write([]byte(stmt))
189 w.wr.Write([]byte{'.', '\n', '\n'})
191 w.wr.Write([]byte{',', '\n'})
197 if diag.Detail != "" {
198 detail := diag.Detail
200 detail = wordwrap.WrapString(detail, w.width)
202 fmt.Fprintf(w.wr, "%s\n\n", detail)
208 func (w *diagnosticTextWriter) WriteDiagnostics(diags Diagnostics) error {
209 for _, diag := range diags {
210 err := w.WriteDiagnostic(diag)
218 func (w *diagnosticTextWriter) traversalStr(traversal Traversal) string {
219 // This is a specialized subset of traversal rendering tailored to
220 // producing helpful contextual messages in diagnostics. It is not
221 // comprehensive nor intended to be used for other purposes.
224 for _, step := range traversal {
225 switch tStep := step.(type) {
227 buf.WriteString(tStep.Name)
230 buf.WriteString(tStep.Name)
233 if keyTy := tStep.Key.Type(); keyTy.IsPrimitiveType() {
234 buf.WriteString(w.valueStr(tStep.Key))
236 // We'll just use a placeholder for more complex values,
237 // since otherwise our result could grow ridiculously long.
238 buf.WriteString("...")
246 func (w *diagnosticTextWriter) valueStr(val cty.Value) string {
247 // This is a specialized subset of value rendering tailored to producing
248 // helpful but concise messages in diagnostics. It is not comprehensive
249 // nor intended to be used for other purposes.
256 // Should never happen here because we should filter before we get
257 // in here, but we'll do something reasonable rather than panic.
258 return "(not yet known)"
264 case ty == cty.Number:
265 bf := val.AsBigFloat()
266 return bf.Text('g', 10)
267 case ty == cty.String:
268 // Go string syntax is not exactly the same as HCL native string syntax,
269 // but we'll accept the minor edge-cases where this is different here
270 // for now, just to get something reasonable here.
271 return fmt.Sprintf("%q", val.AsString())
272 case ty.IsCollectionType() || ty.IsTupleType():
276 return "empty " + ty.FriendlyName()
278 return ty.FriendlyName() + " with 1 element"
280 return fmt.Sprintf("%s with %d elements", ty.FriendlyName(), l)
282 case ty.IsObjectType():
283 atys := ty.AttributeTypes()
287 return "object with no attributes"
290 for k := range atys {
293 return fmt.Sprintf("object with 1 attribute %q", name)
295 return fmt.Sprintf("object with %d attributes", l)
298 return ty.FriendlyName()
302 func contextString(file *File, offset int) string {
303 type contextStringer interface {
304 ContextString(offset int) string
307 if cser, ok := file.Nav.(contextStringer); ok {
308 return cser.ContextString(offset)