github.com/hashicorp/hcl/v2@v2.20.0/diagnostic_text.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package hcl 5 6 import ( 7 "bufio" 8 "bytes" 9 "errors" 10 "fmt" 11 "io" 12 "sort" 13 14 wordwrap "github.com/mitchellh/go-wordwrap" 15 "github.com/zclconf/go-cty/cty" 16 ) 17 18 type diagnosticTextWriter struct { 19 files map[string]*File 20 wr io.Writer 21 width uint 22 color bool 23 } 24 25 // NewDiagnosticTextWriter creates a DiagnosticWriter that writes diagnostics 26 // to the given writer as formatted text. 27 // 28 // It is designed to produce text appropriate to print in a monospaced font 29 // in a terminal of a particular width, or optionally with no width limit. 30 // 31 // The given width may be zero to disable word-wrapping of the detail text 32 // and truncation of source code snippets. 33 // 34 // If color is set to true, the output will include VT100 escape sequences to 35 // color-code the severity indicators. It is suggested to turn this off if 36 // the target writer is not a terminal. 37 func NewDiagnosticTextWriter(wr io.Writer, files map[string]*File, width uint, color bool) DiagnosticWriter { 38 return &diagnosticTextWriter{ 39 files: files, 40 wr: wr, 41 width: width, 42 color: color, 43 } 44 } 45 46 func (w *diagnosticTextWriter) WriteDiagnostic(diag *Diagnostic) error { 47 if diag == nil { 48 return errors.New("nil diagnostic") 49 } 50 51 var colorCode, highlightCode, resetCode string 52 if w.color { 53 switch diag.Severity { 54 case DiagError: 55 colorCode = "\x1b[31m" 56 case DiagWarning: 57 colorCode = "\x1b[33m" 58 } 59 resetCode = "\x1b[0m" 60 highlightCode = "\x1b[1;4m" 61 } 62 63 var severityStr string 64 switch diag.Severity { 65 case DiagError: 66 severityStr = "Error" 67 case DiagWarning: 68 severityStr = "Warning" 69 default: 70 // should never happen 71 severityStr = "???????" 72 } 73 74 fmt.Fprintf(w.wr, "%s%s%s: %s\n\n", colorCode, severityStr, resetCode, diag.Summary) 75 76 if diag.Subject != nil { 77 snipRange := *diag.Subject 78 highlightRange := snipRange 79 if diag.Context != nil { 80 // Show enough of the source code to include both the subject 81 // and context ranges, which overlap in all reasonable 82 // situations. 83 snipRange = RangeOver(snipRange, *diag.Context) 84 } 85 // We can't illustrate an empty range, so we'll turn such ranges into 86 // single-character ranges, which might not be totally valid (may point 87 // off the end of a line, or off the end of the file) but are good 88 // enough for the bounds checks we do below. 89 if snipRange.Empty() { 90 snipRange.End.Byte++ 91 snipRange.End.Column++ 92 } 93 if highlightRange.Empty() { 94 highlightRange.End.Byte++ 95 highlightRange.End.Column++ 96 } 97 98 file := w.files[diag.Subject.Filename] 99 if file == nil || file.Bytes == nil { 100 fmt.Fprintf(w.wr, " on %s line %d:\n (source code not available)\n\n", diag.Subject.Filename, diag.Subject.Start.Line) 101 } else { 102 103 var contextLine string 104 if diag.Subject != nil { 105 contextLine = contextString(file, diag.Subject.Start.Byte) 106 if contextLine != "" { 107 contextLine = ", in " + contextLine 108 } 109 } 110 111 fmt.Fprintf(w.wr, " on %s line %d%s:\n", diag.Subject.Filename, diag.Subject.Start.Line, contextLine) 112 113 src := file.Bytes 114 sc := NewRangeScanner(src, diag.Subject.Filename, bufio.ScanLines) 115 116 for sc.Scan() { 117 lineRange := sc.Range() 118 if !lineRange.Overlaps(snipRange) { 119 continue 120 } 121 122 beforeRange, highlightedRange, afterRange := lineRange.PartitionAround(highlightRange) 123 if highlightedRange.Empty() { 124 fmt.Fprintf(w.wr, "%4d: %s\n", lineRange.Start.Line, sc.Bytes()) 125 } else { 126 before := beforeRange.SliceBytes(src) 127 highlighted := highlightedRange.SliceBytes(src) 128 after := afterRange.SliceBytes(src) 129 fmt.Fprintf( 130 w.wr, "%4d: %s%s%s%s%s\n", 131 lineRange.Start.Line, 132 before, 133 highlightCode, highlighted, resetCode, 134 after, 135 ) 136 } 137 138 } 139 140 w.wr.Write([]byte{'\n'}) 141 } 142 143 if diag.Expression != nil && diag.EvalContext != nil { 144 // We will attempt to render the values for any variables 145 // referenced in the given expression as additional context, for 146 // situations where the same expression is evaluated multiple 147 // times in different scopes. 148 expr := diag.Expression 149 ctx := diag.EvalContext 150 151 vars := expr.Variables() 152 stmts := make([]string, 0, len(vars)) 153 seen := make(map[string]struct{}, len(vars)) 154 for _, traversal := range vars { 155 val, diags := traversal.TraverseAbs(ctx) 156 if diags.HasErrors() { 157 // Skip anything that generates errors, since we probably 158 // already have the same error in our diagnostics set 159 // already. 160 continue 161 } 162 163 traversalStr := w.traversalStr(traversal) 164 if _, exists := seen[traversalStr]; exists { 165 continue // don't show duplicates when the same variable is referenced multiple times 166 } 167 switch { 168 case !val.IsKnown(): 169 // Can't say anything about this yet, then. 170 continue 171 case val.IsNull(): 172 stmts = append(stmts, fmt.Sprintf("%s set to null", traversalStr)) 173 default: 174 stmts = append(stmts, fmt.Sprintf("%s as %s", traversalStr, w.valueStr(val))) 175 } 176 seen[traversalStr] = struct{}{} 177 } 178 179 sort.Strings(stmts) // FIXME: Should maybe use a traversal-aware sort that can sort numeric indexes properly? 180 last := len(stmts) - 1 181 182 for i, stmt := range stmts { 183 switch i { 184 case 0: 185 w.wr.Write([]byte{'w', 'i', 't', 'h', ' '}) 186 default: 187 w.wr.Write([]byte{' ', ' ', ' ', ' ', ' '}) 188 } 189 w.wr.Write([]byte(stmt)) 190 switch i { 191 case last: 192 w.wr.Write([]byte{'.', '\n', '\n'}) 193 default: 194 w.wr.Write([]byte{',', '\n'}) 195 } 196 } 197 } 198 } 199 200 if diag.Detail != "" { 201 detail := diag.Detail 202 if w.width != 0 { 203 detail = wordwrap.WrapString(detail, w.width) 204 } 205 fmt.Fprintf(w.wr, "%s\n\n", detail) 206 } 207 208 return nil 209 } 210 211 func (w *diagnosticTextWriter) WriteDiagnostics(diags Diagnostics) error { 212 for _, diag := range diags { 213 err := w.WriteDiagnostic(diag) 214 if err != nil { 215 return err 216 } 217 } 218 return nil 219 } 220 221 func (w *diagnosticTextWriter) traversalStr(traversal Traversal) string { 222 // This is a specialized subset of traversal rendering tailored to 223 // producing helpful contextual messages in diagnostics. It is not 224 // comprehensive nor intended to be used for other purposes. 225 226 var buf bytes.Buffer 227 for _, step := range traversal { 228 switch tStep := step.(type) { 229 case TraverseRoot: 230 buf.WriteString(tStep.Name) 231 case TraverseAttr: 232 buf.WriteByte('.') 233 buf.WriteString(tStep.Name) 234 case TraverseIndex: 235 buf.WriteByte('[') 236 if keyTy := tStep.Key.Type(); keyTy.IsPrimitiveType() { 237 buf.WriteString(w.valueStr(tStep.Key)) 238 } else { 239 // We'll just use a placeholder for more complex values, 240 // since otherwise our result could grow ridiculously long. 241 buf.WriteString("...") 242 } 243 buf.WriteByte(']') 244 } 245 } 246 return buf.String() 247 } 248 249 func (w *diagnosticTextWriter) valueStr(val cty.Value) string { 250 // This is a specialized subset of value rendering tailored to producing 251 // helpful but concise messages in diagnostics. It is not comprehensive 252 // nor intended to be used for other purposes. 253 254 ty := val.Type() 255 switch { 256 case val.IsNull(): 257 return "null" 258 case !val.IsKnown(): 259 // Should never happen here because we should filter before we get 260 // in here, but we'll do something reasonable rather than panic. 261 return "(not yet known)" 262 case ty == cty.Bool: 263 if val.True() { 264 return "true" 265 } 266 return "false" 267 case ty == cty.Number: 268 bf := val.AsBigFloat() 269 return bf.Text('g', 10) 270 case ty == cty.String: 271 // Go string syntax is not exactly the same as HCL native string syntax, 272 // but we'll accept the minor edge-cases where this is different here 273 // for now, just to get something reasonable here. 274 return fmt.Sprintf("%q", val.AsString()) 275 case ty.IsCollectionType() || ty.IsTupleType(): 276 l := val.LengthInt() 277 switch l { 278 case 0: 279 return "empty " + ty.FriendlyName() 280 case 1: 281 return ty.FriendlyName() + " with 1 element" 282 default: 283 return fmt.Sprintf("%s with %d elements", ty.FriendlyName(), l) 284 } 285 case ty.IsObjectType(): 286 atys := ty.AttributeTypes() 287 l := len(atys) 288 switch l { 289 case 0: 290 return "object with no attributes" 291 case 1: 292 var name string 293 for k := range atys { 294 name = k 295 } 296 return fmt.Sprintf("object with 1 attribute %q", name) 297 default: 298 return fmt.Sprintf("object with %d attributes", l) 299 } 300 default: 301 return ty.FriendlyName() 302 } 303 } 304 305 func contextString(file *File, offset int) string { 306 type contextStringer interface { 307 ContextString(offset int) string 308 } 309 310 if cser, ok := file.Nav.(contextStringer); ok { 311 return cser.ContextString(offset) 312 } 313 return "" 314 }