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  }