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  }