github.com/hashicorp/terraform-plugin-sdk@v1.17.2/internal/command/format/diagnostic.go (about) 1 package format 2 3 import ( 4 "bufio" 5 "bytes" 6 "fmt" 7 "sort" 8 "strings" 9 10 "github.com/hashicorp/hcl/v2" 11 "github.com/hashicorp/hcl/v2/hcled" 12 "github.com/hashicorp/hcl/v2/hclparse" 13 "github.com/hashicorp/terraform-plugin-sdk/internal/tfdiags" 14 "github.com/mitchellh/colorstring" 15 wordwrap "github.com/mitchellh/go-wordwrap" 16 "github.com/zclconf/go-cty/cty" 17 ) 18 19 // Diagnostic formats a single diagnostic message. 20 // 21 // The width argument specifies at what column the diagnostic messages will 22 // be wrapped. If set to zero, messages will not be wrapped by this function 23 // at all. Although the long-form text parts of the message are wrapped, 24 // not all aspects of the message are guaranteed to fit within the specified 25 // terminal width. 26 func Diagnostic(diag tfdiags.Diagnostic, sources map[string][]byte, color *colorstring.Colorize, width int) string { 27 if diag == nil { 28 // No good reason to pass a nil diagnostic in here... 29 return "" 30 } 31 32 var buf bytes.Buffer 33 34 switch diag.Severity() { 35 case tfdiags.Error: 36 buf.WriteString(color.Color("\n[bold][red]Error: [reset]")) 37 case tfdiags.Warning: 38 buf.WriteString(color.Color("\n[bold][yellow]Warning: [reset]")) 39 default: 40 // Clear out any coloring that might be applied by Terraform's UI helper, 41 // so our result is not context-sensitive. 42 buf.WriteString(color.Color("\n[reset]")) 43 } 44 45 desc := diag.Description() 46 sourceRefs := diag.Source() 47 48 // We don't wrap the summary, since we expect it to be terse, and since 49 // this is where we put the text of a native Go error it may not always 50 // be pure text that lends itself well to word-wrapping. 51 fmt.Fprintf(&buf, color.Color("[bold]%s[reset]\n\n"), desc.Summary) 52 53 if sourceRefs.Subject != nil { 54 // We'll borrow HCL's range implementation here, because it has some 55 // handy features to help us produce a nice source code snippet. 56 highlightRange := sourceRefs.Subject.ToHCL() 57 snippetRange := highlightRange 58 if sourceRefs.Context != nil { 59 snippetRange = sourceRefs.Context.ToHCL() 60 } 61 62 // Make sure the snippet includes the highlight. This should be true 63 // for any reasonable diagnostic, but we'll make sure. 64 snippetRange = hcl.RangeOver(snippetRange, highlightRange) 65 if snippetRange.Empty() { 66 snippetRange.End.Byte++ 67 snippetRange.End.Column++ 68 } 69 if highlightRange.Empty() { 70 highlightRange.End.Byte++ 71 highlightRange.End.Column++ 72 } 73 74 var src []byte 75 if sources != nil { 76 src = sources[snippetRange.Filename] 77 } 78 if src == nil { 79 // This should generally not happen, as long as sources are always 80 // loaded through the main loader. We may load things in other 81 // ways in weird cases, so we'll tolerate it at the expense of 82 // a not-so-helpful error message. 83 fmt.Fprintf(&buf, " on %s line %d:\n (source code not available)\n", highlightRange.Filename, highlightRange.Start.Line) 84 } else { 85 file, offset := parseRange(src, highlightRange) 86 87 headerRange := highlightRange 88 89 contextStr := hcled.ContextString(file, offset-1) 90 if contextStr != "" { 91 contextStr = ", in " + contextStr 92 } 93 94 fmt.Fprintf(&buf, " on %s line %d%s:\n", headerRange.Filename, headerRange.Start.Line, contextStr) 95 96 // Config snippet rendering 97 sc := hcl.NewRangeScanner(src, highlightRange.Filename, bufio.ScanLines) 98 for sc.Scan() { 99 lineRange := sc.Range() 100 if !lineRange.Overlaps(snippetRange) { 101 continue 102 } 103 beforeRange, highlightedRange, afterRange := lineRange.PartitionAround(highlightRange) 104 before := beforeRange.SliceBytes(src) 105 highlighted := highlightedRange.SliceBytes(src) 106 after := afterRange.SliceBytes(src) 107 fmt.Fprintf( 108 &buf, color.Color("%4d: %s[underline]%s[reset]%s\n"), 109 lineRange.Start.Line, 110 before, highlighted, after, 111 ) 112 } 113 114 } 115 116 if fromExpr := diag.FromExpr(); fromExpr != nil { 117 // We may also be able to generate information about the dynamic 118 // values of relevant variables at the point of evaluation, then. 119 // This is particularly useful for expressions that get evaluated 120 // multiple times with different values, such as blocks using 121 // "count" and "for_each", or within "for" expressions. 122 expr := fromExpr.Expression 123 ctx := fromExpr.EvalContext 124 vars := expr.Variables() 125 stmts := make([]string, 0, len(vars)) 126 seen := make(map[string]struct{}, len(vars)) 127 Traversals: 128 for _, traversal := range vars { 129 for len(traversal) > 1 { 130 val, diags := traversal.TraverseAbs(ctx) 131 if diags.HasErrors() { 132 // Skip anything that generates errors, since we probably 133 // already have the same error in our diagnostics set 134 // already. 135 traversal = traversal[:len(traversal)-1] 136 continue 137 } 138 139 traversalStr := traversalStr(traversal) 140 if _, exists := seen[traversalStr]; exists { 141 continue Traversals // don't show duplicates when the same variable is referenced multiple times 142 } 143 switch { 144 case !val.IsKnown(): 145 // Can't say anything about this yet, then. 146 continue Traversals 147 case val.IsNull(): 148 stmts = append(stmts, fmt.Sprintf(color.Color("[bold]%s[reset] is null"), traversalStr)) 149 default: 150 stmts = append(stmts, fmt.Sprintf(color.Color("[bold]%s[reset] is %s"), traversalStr, compactValueStr(val))) 151 } 152 seen[traversalStr] = struct{}{} 153 } 154 } 155 156 sort.Strings(stmts) // FIXME: Should maybe use a traversal-aware sort that can sort numeric indexes properly? 157 158 if len(stmts) > 0 { 159 fmt.Fprint(&buf, color.Color(" [dark_gray]|----------------[reset]\n")) 160 } 161 for _, stmt := range stmts { 162 fmt.Fprintf(&buf, color.Color(" [dark_gray]|[reset] %s\n"), stmt) 163 } 164 } 165 166 buf.WriteByte('\n') 167 } 168 169 if desc.Detail != "" { 170 detail := desc.Detail 171 if width != 0 { 172 detail = wordwrap.WrapString(detail, uint(width)) 173 } 174 fmt.Fprintf(&buf, "%s\n", detail) 175 } 176 177 return buf.String() 178 } 179 180 func parseRange(src []byte, rng hcl.Range) (*hcl.File, int) { 181 filename := rng.Filename 182 offset := rng.Start.Byte 183 184 // We need to re-parse here to get a *hcl.File we can interrogate. This 185 // is not awesome since we presumably already parsed the file earlier too, 186 // but this re-parsing is architecturally simpler than retaining all of 187 // the hcl.File objects and we only do this in the case of an error anyway 188 // so the overhead here is not a big problem. 189 parser := hclparse.NewParser() 190 var file *hcl.File 191 var diags hcl.Diagnostics 192 if strings.HasSuffix(filename, ".json") { 193 file, diags = parser.ParseJSON(src, filename) 194 } else { 195 file, diags = parser.ParseHCL(src, filename) 196 } 197 if diags.HasErrors() { 198 return file, offset 199 } 200 201 return file, offset 202 } 203 204 // traversalStr produces a representation of an HCL traversal that is compact, 205 // resembles HCL native syntax, and is suitable for display in the UI. 206 func traversalStr(traversal hcl.Traversal) string { 207 // This is a specialized subset of traversal rendering tailored to 208 // producing helpful contextual messages in diagnostics. It is not 209 // comprehensive nor intended to be used for other purposes. 210 211 var buf bytes.Buffer 212 for _, step := range traversal { 213 switch tStep := step.(type) { 214 case hcl.TraverseRoot: 215 buf.WriteString(tStep.Name) 216 case hcl.TraverseAttr: 217 buf.WriteByte('.') 218 buf.WriteString(tStep.Name) 219 case hcl.TraverseIndex: 220 buf.WriteByte('[') 221 if keyTy := tStep.Key.Type(); keyTy.IsPrimitiveType() { 222 buf.WriteString(compactValueStr(tStep.Key)) 223 } else { 224 // We'll just use a placeholder for more complex values, 225 // since otherwise our result could grow ridiculously long. 226 buf.WriteString("...") 227 } 228 buf.WriteByte(']') 229 } 230 } 231 return buf.String() 232 } 233 234 // compactValueStr produces a compact, single-line summary of a given value 235 // that is suitable for display in the UI. 236 // 237 // For primitives it returns a full representation, while for more complex 238 // types it instead summarizes the type, size, etc to produce something 239 // that is hopefully still somewhat useful but not as verbose as a rendering 240 // of the entire data structure. 241 func compactValueStr(val cty.Value) string { 242 // This is a specialized subset of value rendering tailored to producing 243 // helpful but concise messages in diagnostics. It is not comprehensive 244 // nor intended to be used for other purposes. 245 246 ty := val.Type() 247 switch { 248 case val.IsNull(): 249 return "null" 250 case !val.IsKnown(): 251 // Should never happen here because we should filter before we get 252 // in here, but we'll do something reasonable rather than panic. 253 return "(not yet known)" 254 case ty == cty.Bool: 255 if val.True() { 256 return "true" 257 } 258 return "false" 259 case ty == cty.Number: 260 bf := val.AsBigFloat() 261 return bf.Text('g', 10) 262 case ty == cty.String: 263 // Go string syntax is not exactly the same as HCL native string syntax, 264 // but we'll accept the minor edge-cases where this is different here 265 // for now, just to get something reasonable here. 266 return fmt.Sprintf("%q", val.AsString()) 267 case ty.IsCollectionType() || ty.IsTupleType(): 268 l := val.LengthInt() 269 switch l { 270 case 0: 271 return "empty " + ty.FriendlyName() 272 case 1: 273 return ty.FriendlyName() + " with 1 element" 274 default: 275 return fmt.Sprintf("%s with %d elements", ty.FriendlyName(), l) 276 } 277 case ty.IsObjectType(): 278 atys := ty.AttributeTypes() 279 l := len(atys) 280 switch l { 281 case 0: 282 return "object with no attributes" 283 case 1: 284 var name string 285 for k := range atys { 286 name = k 287 } 288 return fmt.Sprintf("object with 1 attribute %q", name) 289 default: 290 return fmt.Sprintf("object with %d attributes", l) 291 } 292 default: 293 return ty.FriendlyName() 294 } 295 }