github.com/hugorut/terraform@v1.1.3/src/command/views/json/diagnostic.go (about) 1 package json 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/hugorut/terraform/src/lang/marks" 14 "github.com/hugorut/terraform/src/tfdiags" 15 "github.com/zclconf/go-cty/cty" 16 ) 17 18 // These severities map to the tfdiags.Severity values, plus an explicit 19 // unknown in case that enum grows without us noticing here. 20 const ( 21 DiagnosticSeverityUnknown = "unknown" 22 DiagnosticSeverityError = "error" 23 DiagnosticSeverityWarning = "warning" 24 ) 25 26 // Diagnostic represents any tfdiags.Diagnostic value. The simplest form has 27 // just a severity, single line summary, and optional detail. If there is more 28 // information about the source of the diagnostic, this is represented in the 29 // range field. 30 type Diagnostic struct { 31 Severity string `json:"severity"` 32 Summary string `json:"summary"` 33 Detail string `json:"detail"` 34 Address string `json:"address,omitempty"` 35 Range *DiagnosticRange `json:"range,omitempty"` 36 Snippet *DiagnosticSnippet `json:"snippet,omitempty"` 37 } 38 39 // Pos represents a position in the source code. 40 type Pos struct { 41 // Line is a one-based count for the line in the indicated file. 42 Line int `json:"line"` 43 44 // Column is a one-based count of Unicode characters from the start of the line. 45 Column int `json:"column"` 46 47 // Byte is a zero-based offset into the indicated file. 48 Byte int `json:"byte"` 49 } 50 51 // DiagnosticRange represents the filename and position of the diagnostic 52 // subject. This defines the range of the source to be highlighted in the 53 // output. Note that the snippet may include additional surrounding source code 54 // if the diagnostic has a context range. 55 // 56 // The Start position is inclusive, and the End position is exclusive. Exact 57 // positions are intended for highlighting for human interpretation only and 58 // are subject to change. 59 type DiagnosticRange struct { 60 Filename string `json:"filename"` 61 Start Pos `json:"start"` 62 End Pos `json:"end"` 63 } 64 65 // DiagnosticSnippet represents source code information about the diagnostic. 66 // It is possible for a diagnostic to have a source (and therefore a range) but 67 // no source code can be found. In this case, the range field will be present and 68 // the snippet field will not. 69 type DiagnosticSnippet struct { 70 // Context is derived from HCL's hcled.ContextString output. This gives a 71 // high-level summary of the root context of the diagnostic: for example, 72 // the resource block in which an expression causes an error. 73 Context *string `json:"context"` 74 75 // Code is a possibly-multi-line string of Terraform configuration, which 76 // includes both the diagnostic source and any relevant context as defined 77 // by the diagnostic. 78 Code string `json:"code"` 79 80 // StartLine is the line number in the source file for the first line of 81 // the snippet code block. This is not necessarily the same as the value of 82 // Range.Start.Line, as it is possible to have zero or more lines of 83 // context source code before the diagnostic range starts. 84 StartLine int `json:"start_line"` 85 86 // HighlightStartOffset is the character offset into Code at which the 87 // diagnostic source range starts, which ought to be highlighted as such by 88 // the consumer of this data. 89 HighlightStartOffset int `json:"highlight_start_offset"` 90 91 // HighlightEndOffset is the character offset into Code at which the 92 // diagnostic source range ends. 93 HighlightEndOffset int `json:"highlight_end_offset"` 94 95 // Values is a sorted slice of expression values which may be useful in 96 // understanding the source of an error in a complex expression. 97 Values []DiagnosticExpressionValue `json:"values"` 98 } 99 100 // DiagnosticExpressionValue represents an HCL traversal string (e.g. 101 // "var.foo") and a statement about its value while the expression was 102 // evaluated (e.g. "is a string", "will be known only after apply"). These are 103 // intended to help the consumer diagnose why an expression caused a diagnostic 104 // to be emitted. 105 type DiagnosticExpressionValue struct { 106 Traversal string `json:"traversal"` 107 Statement string `json:"statement"` 108 } 109 110 // NewDiagnostic takes a tfdiags.Diagnostic and a map of configuration sources, 111 // and returns a Diagnostic struct. 112 func NewDiagnostic(diag tfdiags.Diagnostic, sources map[string][]byte) *Diagnostic { 113 var sev string 114 switch diag.Severity() { 115 case tfdiags.Error: 116 sev = DiagnosticSeverityError 117 case tfdiags.Warning: 118 sev = DiagnosticSeverityWarning 119 default: 120 sev = DiagnosticSeverityUnknown 121 } 122 123 desc := diag.Description() 124 125 diagnostic := &Diagnostic{ 126 Severity: sev, 127 Summary: desc.Summary, 128 Detail: desc.Detail, 129 Address: desc.Address, 130 } 131 132 sourceRefs := diag.Source() 133 if sourceRefs.Subject != nil { 134 // We'll borrow HCL's range implementation here, because it has some 135 // handy features to help us produce a nice source code snippet. 136 highlightRange := sourceRefs.Subject.ToHCL() 137 138 // Some diagnostic sources fail to set the end of the subject range. 139 if highlightRange.End == (hcl.Pos{}) { 140 highlightRange.End = highlightRange.Start 141 } 142 143 snippetRange := highlightRange 144 if sourceRefs.Context != nil { 145 snippetRange = sourceRefs.Context.ToHCL() 146 } 147 148 // Make sure the snippet includes the highlight. This should be true 149 // for any reasonable diagnostic, but we'll make sure. 150 snippetRange = hcl.RangeOver(snippetRange, highlightRange) 151 152 // Empty ranges result in odd diagnostic output, so extend the end to 153 // ensure there's at least one byte in the snippet or highlight. 154 if snippetRange.Empty() { 155 snippetRange.End.Byte++ 156 snippetRange.End.Column++ 157 } 158 if highlightRange.Empty() { 159 highlightRange.End.Byte++ 160 highlightRange.End.Column++ 161 } 162 163 diagnostic.Range = &DiagnosticRange{ 164 Filename: highlightRange.Filename, 165 Start: Pos{ 166 Line: highlightRange.Start.Line, 167 Column: highlightRange.Start.Column, 168 Byte: highlightRange.Start.Byte, 169 }, 170 End: Pos{ 171 Line: highlightRange.End.Line, 172 Column: highlightRange.End.Column, 173 Byte: highlightRange.End.Byte, 174 }, 175 } 176 177 var src []byte 178 if sources != nil { 179 src = sources[highlightRange.Filename] 180 } 181 182 // If we have a source file for the diagnostic, we can emit a code 183 // snippet. 184 if src != nil { 185 diagnostic.Snippet = &DiagnosticSnippet{ 186 StartLine: snippetRange.Start.Line, 187 188 // Ensure that the default Values struct is an empty array, as this 189 // makes consuming the JSON structure easier in most languages. 190 Values: []DiagnosticExpressionValue{}, 191 } 192 193 file, offset := parseRange(src, highlightRange) 194 195 // Some diagnostics may have a useful top-level context to add to 196 // the code snippet output. 197 contextStr := hcled.ContextString(file, offset-1) 198 if contextStr != "" { 199 diagnostic.Snippet.Context = &contextStr 200 } 201 202 // Build the string of the code snippet, tracking at which byte of 203 // the file the snippet starts. 204 var codeStartByte int 205 sc := hcl.NewRangeScanner(src, highlightRange.Filename, bufio.ScanLines) 206 var code strings.Builder 207 for sc.Scan() { 208 lineRange := sc.Range() 209 if lineRange.Overlaps(snippetRange) { 210 if codeStartByte == 0 && code.Len() == 0 { 211 codeStartByte = lineRange.Start.Byte 212 } 213 code.Write(lineRange.SliceBytes(src)) 214 code.WriteRune('\n') 215 } 216 } 217 codeStr := strings.TrimSuffix(code.String(), "\n") 218 diagnostic.Snippet.Code = codeStr 219 220 // Calculate the start and end byte of the highlight range relative 221 // to the code snippet string. 222 start := highlightRange.Start.Byte - codeStartByte 223 end := start + (highlightRange.End.Byte - highlightRange.Start.Byte) 224 225 // We can end up with some quirky results here in edge cases like 226 // when a source range starts or ends at a newline character, 227 // so we'll cap the results at the bounds of the highlight range 228 // so that consumers of this data don't need to contend with 229 // out-of-bounds errors themselves. 230 if start < 0 { 231 start = 0 232 } else if start > len(codeStr) { 233 start = len(codeStr) 234 } 235 if end < 0 { 236 end = 0 237 } else if end > len(codeStr) { 238 end = len(codeStr) 239 } 240 241 diagnostic.Snippet.HighlightStartOffset = start 242 diagnostic.Snippet.HighlightEndOffset = end 243 244 if fromExpr := diag.FromExpr(); fromExpr != nil { 245 // We may also be able to generate information about the dynamic 246 // values of relevant variables at the point of evaluation, then. 247 // This is particularly useful for expressions that get evaluated 248 // multiple times with different values, such as blocks using 249 // "count" and "for_each", or within "for" expressions. 250 expr := fromExpr.Expression 251 ctx := fromExpr.EvalContext 252 vars := expr.Variables() 253 values := make([]DiagnosticExpressionValue, 0, len(vars)) 254 seen := make(map[string]struct{}, len(vars)) 255 Traversals: 256 for _, traversal := range vars { 257 for len(traversal) > 1 { 258 val, diags := traversal.TraverseAbs(ctx) 259 if diags.HasErrors() { 260 // Skip anything that generates errors, since we probably 261 // already have the same error in our diagnostics set 262 // already. 263 traversal = traversal[:len(traversal)-1] 264 continue 265 } 266 267 traversalStr := traversalStr(traversal) 268 if _, exists := seen[traversalStr]; exists { 269 continue Traversals // don't show duplicates when the same variable is referenced multiple times 270 } 271 value := DiagnosticExpressionValue{ 272 Traversal: traversalStr, 273 } 274 switch { 275 case val.HasMark(marks.Sensitive): 276 // We won't say anything at all about sensitive values, 277 // because we might give away something that was 278 // sensitive about them. 279 value.Statement = "has a sensitive value" 280 case !val.IsKnown(): 281 if ty := val.Type(); ty != cty.DynamicPseudoType { 282 value.Statement = fmt.Sprintf("is a %s, known only after apply", ty.FriendlyName()) 283 } else { 284 value.Statement = "will be known only after apply" 285 } 286 default: 287 value.Statement = fmt.Sprintf("is %s", compactValueStr(val)) 288 } 289 values = append(values, value) 290 seen[traversalStr] = struct{}{} 291 } 292 } 293 sort.Slice(values, func(i, j int) bool { 294 return values[i].Traversal < values[j].Traversal 295 }) 296 diagnostic.Snippet.Values = values 297 } 298 } 299 } 300 301 return diagnostic 302 } 303 304 func parseRange(src []byte, rng hcl.Range) (*hcl.File, int) { 305 filename := rng.Filename 306 offset := rng.Start.Byte 307 308 // We need to re-parse here to get a *hcl.File we can interrogate. This 309 // is not awesome since we presumably already parsed the file earlier too, 310 // but this re-parsing is architecturally simpler than retaining all of 311 // the hcl.File objects and we only do this in the case of an error anyway 312 // so the overhead here is not a big problem. 313 parser := hclparse.NewParser() 314 var file *hcl.File 315 316 // Ignore diagnostics here as there is nothing we can do with them. 317 if strings.HasSuffix(filename, ".json") { 318 file, _ = parser.ParseJSON(src, filename) 319 } else { 320 file, _ = parser.ParseHCL(src, filename) 321 } 322 323 return file, offset 324 } 325 326 // compactValueStr produces a compact, single-line summary of a given value 327 // that is suitable for display in the UI. 328 // 329 // For primitives it returns a full representation, while for more complex 330 // types it instead summarizes the type, size, etc to produce something 331 // that is hopefully still somewhat useful but not as verbose as a rendering 332 // of the entire data structure. 333 func compactValueStr(val cty.Value) string { 334 // This is a specialized subset of value rendering tailored to producing 335 // helpful but concise messages in diagnostics. It is not comprehensive 336 // nor intended to be used for other purposes. 337 338 if val.HasMark(marks.Sensitive) { 339 // We check this in here just to make sure, but note that the caller 340 // of compactValueStr ought to have already checked this and skipped 341 // calling into compactValueStr anyway, so this shouldn't actually 342 // be reachable. 343 return "(sensitive value)" 344 } 345 346 // WARNING: We've only checked that the value isn't sensitive _shallowly_ 347 // here, and so we must never show any element values from complex types 348 // in here. However, it's fine to show map keys and attribute names because 349 // those are never sensitive in isolation: the entire value would be 350 // sensitive in that case. 351 352 ty := val.Type() 353 switch { 354 case val.IsNull(): 355 return "null" 356 case !val.IsKnown(): 357 // Should never happen here because we should filter before we get 358 // in here, but we'll do something reasonable rather than panic. 359 return "(not yet known)" 360 case ty == cty.Bool: 361 if val.True() { 362 return "true" 363 } 364 return "false" 365 case ty == cty.Number: 366 bf := val.AsBigFloat() 367 return bf.Text('g', 10) 368 case ty == cty.String: 369 // Go string syntax is not exactly the same as HCL native string syntax, 370 // but we'll accept the minor edge-cases where this is different here 371 // for now, just to get something reasonable here. 372 return fmt.Sprintf("%q", val.AsString()) 373 case ty.IsCollectionType() || ty.IsTupleType(): 374 l := val.LengthInt() 375 switch l { 376 case 0: 377 return "empty " + ty.FriendlyName() 378 case 1: 379 return ty.FriendlyName() + " with 1 element" 380 default: 381 return fmt.Sprintf("%s with %d elements", ty.FriendlyName(), l) 382 } 383 case ty.IsObjectType(): 384 atys := ty.AttributeTypes() 385 l := len(atys) 386 switch l { 387 case 0: 388 return "object with no attributes" 389 case 1: 390 var name string 391 for k := range atys { 392 name = k 393 } 394 return fmt.Sprintf("object with 1 attribute %q", name) 395 default: 396 return fmt.Sprintf("object with %d attributes", l) 397 } 398 default: 399 return ty.FriendlyName() 400 } 401 } 402 403 // traversalStr produces a representation of an HCL traversal that is compact, 404 // resembles HCL native syntax, and is suitable for display in the UI. 405 func traversalStr(traversal hcl.Traversal) string { 406 // This is a specialized subset of traversal rendering tailored to 407 // producing helpful contextual messages in diagnostics. It is not 408 // comprehensive nor intended to be used for other purposes. 409 410 var buf bytes.Buffer 411 for _, step := range traversal { 412 switch tStep := step.(type) { 413 case hcl.TraverseRoot: 414 buf.WriteString(tStep.Name) 415 case hcl.TraverseAttr: 416 buf.WriteByte('.') 417 buf.WriteString(tStep.Name) 418 case hcl.TraverseIndex: 419 buf.WriteByte('[') 420 if keyTy := tStep.Key.Type(); keyTy.IsPrimitiveType() { 421 buf.WriteString(compactValueStr(tStep.Key)) 422 } else { 423 // We'll just use a placeholder for more complex values, 424 // since otherwise our result could grow ridiculously long. 425 buf.WriteString("...") 426 } 427 buf.WriteByte(']') 428 } 429 } 430 return buf.String() 431 }