github.com/graywolf-at-work-2/terraform-vendor@v1.4.5/internal/command/format/diagnostic.go (about) 1 package format 2 3 import ( 4 "bufio" 5 "bytes" 6 "fmt" 7 "sort" 8 "strings" 9 10 viewsjson "github.com/hashicorp/terraform/internal/command/views/json" 11 "github.com/hashicorp/terraform/internal/tfdiags" 12 13 "github.com/mitchellh/colorstring" 14 wordwrap "github.com/mitchellh/go-wordwrap" 15 ) 16 17 var disabledColorize = &colorstring.Colorize{ 18 Colors: colorstring.DefaultColors, 19 Disable: true, 20 } 21 22 // Diagnostic formats a single diagnostic message. 23 // 24 // The width argument specifies at what column the diagnostic messages will 25 // be wrapped. If set to zero, messages will not be wrapped by this function 26 // at all. Although the long-form text parts of the message are wrapped, 27 // not all aspects of the message are guaranteed to fit within the specified 28 // terminal width. 29 func Diagnostic(diag tfdiags.Diagnostic, sources map[string][]byte, color *colorstring.Colorize, width int) string { 30 return DiagnosticFromJSON(viewsjson.NewDiagnostic(diag, sources), color, width) 31 } 32 33 func DiagnosticFromJSON(diag *viewsjson.Diagnostic, color *colorstring.Colorize, width int) string { 34 if diag == nil { 35 // No good reason to pass a nil diagnostic in here... 36 return "" 37 } 38 39 var buf bytes.Buffer 40 41 // these leftRule* variables are markers for the beginning of the lines 42 // containing the diagnostic that are intended to help sighted users 43 // better understand the information hierarchy when diagnostics appear 44 // alongside other information or alongside other diagnostics. 45 // 46 // Without this, it seems (based on folks sharing incomplete messages when 47 // asking questions, or including extra content that's not part of the 48 // diagnostic) that some readers have trouble easily identifying which 49 // text belongs to the diagnostic and which does not. 50 var leftRuleLine, leftRuleStart, leftRuleEnd string 51 var leftRuleWidth int // in visual character cells 52 53 switch diag.Severity { 54 case viewsjson.DiagnosticSeverityError: 55 buf.WriteString(color.Color("[bold][red]Error: [reset]")) 56 leftRuleLine = color.Color("[red]│[reset] ") 57 leftRuleStart = color.Color("[red]╷[reset]") 58 leftRuleEnd = color.Color("[red]╵[reset]") 59 leftRuleWidth = 2 60 case viewsjson.DiagnosticSeverityWarning: 61 buf.WriteString(color.Color("[bold][yellow]Warning: [reset]")) 62 leftRuleLine = color.Color("[yellow]│[reset] ") 63 leftRuleStart = color.Color("[yellow]╷[reset]") 64 leftRuleEnd = color.Color("[yellow]╵[reset]") 65 leftRuleWidth = 2 66 default: 67 // Clear out any coloring that might be applied by Terraform's UI helper, 68 // so our result is not context-sensitive. 69 buf.WriteString(color.Color("\n[reset]")) 70 } 71 72 // We don't wrap the summary, since we expect it to be terse, and since 73 // this is where we put the text of a native Go error it may not always 74 // be pure text that lends itself well to word-wrapping. 75 fmt.Fprintf(&buf, color.Color("[bold]%s[reset]\n\n"), diag.Summary) 76 77 appendSourceSnippets(&buf, diag, color) 78 79 if diag.Detail != "" { 80 paraWidth := width - leftRuleWidth - 1 // leave room for the left rule 81 if paraWidth > 0 { 82 lines := strings.Split(diag.Detail, "\n") 83 for _, line := range lines { 84 if !strings.HasPrefix(line, " ") { 85 line = wordwrap.WrapString(line, uint(paraWidth)) 86 } 87 fmt.Fprintf(&buf, "%s\n", line) 88 } 89 } else { 90 fmt.Fprintf(&buf, "%s\n", diag.Detail) 91 } 92 } 93 94 // Before we return, we'll finally add the left rule prefixes to each 95 // line so that the overall message is visually delimited from what's 96 // around it. We'll do that by scanning over what we already generated 97 // and adding the prefix for each line. 98 var ruleBuf strings.Builder 99 sc := bufio.NewScanner(&buf) 100 ruleBuf.WriteString(leftRuleStart) 101 ruleBuf.WriteByte('\n') 102 for sc.Scan() { 103 line := sc.Text() 104 prefix := leftRuleLine 105 if line == "" { 106 // Don't print the space after the line if there would be nothing 107 // after it anyway. 108 prefix = strings.TrimSpace(prefix) 109 } 110 ruleBuf.WriteString(prefix) 111 ruleBuf.WriteString(line) 112 ruleBuf.WriteByte('\n') 113 } 114 ruleBuf.WriteString(leftRuleEnd) 115 ruleBuf.WriteByte('\n') 116 117 return ruleBuf.String() 118 } 119 120 // DiagnosticPlain is an alternative to Diagnostic which minimises the use of 121 // virtual terminal formatting sequences. 122 // 123 // It is intended for use in automation and other contexts in which diagnostic 124 // messages are parsed from the Terraform output. 125 func DiagnosticPlain(diag tfdiags.Diagnostic, sources map[string][]byte, width int) string { 126 return DiagnosticPlainFromJSON(viewsjson.NewDiagnostic(diag, sources), width) 127 } 128 129 func DiagnosticPlainFromJSON(diag *viewsjson.Diagnostic, width int) string { 130 if diag == nil { 131 // No good reason to pass a nil diagnostic in here... 132 return "" 133 } 134 135 var buf bytes.Buffer 136 137 switch diag.Severity { 138 case viewsjson.DiagnosticSeverityError: 139 buf.WriteString("\nError: ") 140 case viewsjson.DiagnosticSeverityWarning: 141 buf.WriteString("\nWarning: ") 142 default: 143 buf.WriteString("\n") 144 } 145 146 // We don't wrap the summary, since we expect it to be terse, and since 147 // this is where we put the text of a native Go error it may not always 148 // be pure text that lends itself well to word-wrapping. 149 fmt.Fprintf(&buf, "%s\n\n", diag.Summary) 150 151 appendSourceSnippets(&buf, diag, disabledColorize) 152 153 if diag.Detail != "" { 154 if width > 1 { 155 lines := strings.Split(diag.Detail, "\n") 156 for _, line := range lines { 157 if !strings.HasPrefix(line, " ") { 158 line = wordwrap.WrapString(line, uint(width-1)) 159 } 160 fmt.Fprintf(&buf, "%s\n", line) 161 } 162 } else { 163 fmt.Fprintf(&buf, "%s\n", diag.Detail) 164 } 165 } 166 167 return buf.String() 168 } 169 170 // DiagnosticWarningsCompact is an alternative to Diagnostic for when all of 171 // the given diagnostics are warnings and we want to show them compactly, 172 // with only two lines per warning and excluding all of the detail information. 173 // 174 // The caller may optionally pre-process the given diagnostics with 175 // ConsolidateWarnings, in which case this function will recognize consolidated 176 // messages and include an indication that they are consolidated. 177 // 178 // Do not pass non-warning diagnostics to this function, or the result will 179 // be nonsense. 180 func DiagnosticWarningsCompact(diags tfdiags.Diagnostics, color *colorstring.Colorize) string { 181 var b strings.Builder 182 b.WriteString(color.Color("[bold][yellow]Warnings:[reset]\n\n")) 183 for _, diag := range diags { 184 sources := tfdiags.WarningGroupSourceRanges(diag) 185 b.WriteString(fmt.Sprintf("- %s\n", diag.Description().Summary)) 186 if len(sources) > 0 { 187 mainSource := sources[0] 188 if mainSource.Subject != nil { 189 if len(sources) > 1 { 190 b.WriteString(fmt.Sprintf( 191 " on %s line %d (and %d more)\n", 192 mainSource.Subject.Filename, 193 mainSource.Subject.Start.Line, 194 len(sources)-1, 195 )) 196 } else { 197 b.WriteString(fmt.Sprintf( 198 " on %s line %d\n", 199 mainSource.Subject.Filename, 200 mainSource.Subject.Start.Line, 201 )) 202 } 203 } else if len(sources) > 1 { 204 b.WriteString(fmt.Sprintf( 205 " (%d occurences of this warning)\n", 206 len(sources), 207 )) 208 } 209 } 210 } 211 212 return b.String() 213 } 214 215 func appendSourceSnippets(buf *bytes.Buffer, diag *viewsjson.Diagnostic, color *colorstring.Colorize) { 216 if diag.Address != "" { 217 fmt.Fprintf(buf, " with %s,\n", diag.Address) 218 } 219 220 if diag.Range == nil { 221 return 222 } 223 224 if diag.Snippet == nil { 225 // This should generally not happen, as long as sources are always 226 // loaded through the main loader. We may load things in other 227 // ways in weird cases, so we'll tolerate it at the expense of 228 // a not-so-helpful error message. 229 fmt.Fprintf(buf, " on %s line %d:\n (source code not available)\n", diag.Range.Filename, diag.Range.Start.Line) 230 } else { 231 snippet := diag.Snippet 232 code := snippet.Code 233 234 var contextStr string 235 if snippet.Context != nil { 236 contextStr = fmt.Sprintf(", in %s", *snippet.Context) 237 } 238 fmt.Fprintf(buf, " on %s line %d%s:\n", diag.Range.Filename, diag.Range.Start.Line, contextStr) 239 240 // Split the snippet and render the highlighted section with underlines 241 start := snippet.HighlightStartOffset 242 end := snippet.HighlightEndOffset 243 244 // Only buggy diagnostics can have an end range before the start, but 245 // we need to ensure we don't crash here if that happens. 246 if end < start { 247 end = start + 1 248 if end > len(code) { 249 end = len(code) 250 } 251 } 252 253 // If either start or end is out of range for the code buffer then 254 // we'll cap them at the bounds just to avoid a panic, although 255 // this would happen only if there's a bug in the code generating 256 // the snippet objects. 257 if start < 0 { 258 start = 0 259 } else if start > len(code) { 260 start = len(code) 261 } 262 if end < 0 { 263 end = 0 264 } else if end > len(code) { 265 end = len(code) 266 } 267 268 before, highlight, after := code[0:start], code[start:end], code[end:] 269 code = fmt.Sprintf(color.Color("%s[underline]%s[reset]%s"), before, highlight, after) 270 271 // Split the snippet into lines and render one at a time 272 lines := strings.Split(code, "\n") 273 for i, line := range lines { 274 fmt.Fprintf( 275 buf, "%4d: %s\n", 276 snippet.StartLine+i, 277 line, 278 ) 279 } 280 281 if len(snippet.Values) > 0 || (snippet.FunctionCall != nil && snippet.FunctionCall.Signature != nil) { 282 // The diagnostic may also have information about the dynamic 283 // values of relevant variables at the point of evaluation. 284 // This is particularly useful for expressions that get evaluated 285 // multiple times with different values, such as blocks using 286 // "count" and "for_each", or within "for" expressions. 287 values := make([]viewsjson.DiagnosticExpressionValue, len(snippet.Values)) 288 copy(values, snippet.Values) 289 sort.Slice(values, func(i, j int) bool { 290 return values[i].Traversal < values[j].Traversal 291 }) 292 293 fmt.Fprint(buf, color.Color(" [dark_gray]├────────────────[reset]\n")) 294 if callInfo := snippet.FunctionCall; callInfo != nil && callInfo.Signature != nil { 295 296 fmt.Fprintf(buf, color.Color(" [dark_gray]│[reset] while calling [bold]%s[reset]("), callInfo.CalledAs) 297 for i, param := range callInfo.Signature.Params { 298 if i > 0 { 299 buf.WriteString(", ") 300 } 301 buf.WriteString(param.Name) 302 } 303 if param := callInfo.Signature.VariadicParam; param != nil { 304 if len(callInfo.Signature.Params) > 0 { 305 buf.WriteString(", ") 306 } 307 buf.WriteString(param.Name) 308 buf.WriteString("...") 309 } 310 buf.WriteString(")\n") 311 } 312 for _, value := range values { 313 fmt.Fprintf(buf, color.Color(" [dark_gray]│[reset] [bold]%s[reset] %s\n"), value.Traversal, value.Statement) 314 } 315 } 316 } 317 318 buf.WriteByte('\n') 319 }