github.com/graywolf-at-work-2/terraform-vendor@v1.4.5/internal/command/views/json/diagnostic_test.go (about)

     1  package json
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"os"
     8  	"path"
     9  	"strings"
    10  	"testing"
    11  
    12  	"github.com/google/go-cmp/cmp"
    13  	"github.com/hashicorp/hcl/v2"
    14  	"github.com/hashicorp/hcl/v2/hcltest"
    15  	"github.com/hashicorp/terraform/internal/lang/marks"
    16  	"github.com/hashicorp/terraform/internal/tfdiags"
    17  	"github.com/zclconf/go-cty/cty"
    18  )
    19  
    20  func TestNewDiagnostic(t *testing.T) {
    21  	// Common HCL for diags with source ranges. This does not have any real
    22  	// semantic errors, but we can synthesize fake HCL errors which will
    23  	// exercise the diagnostic rendering code using this
    24  	sources := map[string][]byte{
    25  		"test.tf": []byte(`resource "test_resource" "test" {
    26    foo = var.boop["hello!"]
    27    bar = {
    28      baz = maybe
    29    }
    30  }
    31  `),
    32  		"short.tf":       []byte("bad source code"),
    33  		"odd-comment.tf": []byte("foo\n\n#\n"),
    34  		"values.tf": []byte(`[
    35    var.a,
    36    var.b,
    37    var.c,
    38    var.d,
    39    var.e,
    40    var.f,
    41    var.g,
    42    var.h,
    43    var.i,
    44    var.j,
    45    var.k,
    46  ]
    47  `),
    48  	}
    49  	testCases := map[string]struct {
    50  		diag interface{} // allow various kinds of diags
    51  		want *Diagnostic
    52  	}{
    53  		"sourceless warning": {
    54  			tfdiags.Sourceless(
    55  				tfdiags.Warning,
    56  				"Oh no",
    57  				"Something is broken",
    58  			),
    59  			&Diagnostic{
    60  				Severity: "warning",
    61  				Summary:  "Oh no",
    62  				Detail:   "Something is broken",
    63  			},
    64  		},
    65  		"error with source code unavailable": {
    66  			&hcl.Diagnostic{
    67  				Severity: hcl.DiagError,
    68  				Summary:  "Bad news",
    69  				Detail:   "It went wrong",
    70  				Subject: &hcl.Range{
    71  					Filename: "modules/oops/missing.tf",
    72  					Start:    hcl.Pos{Line: 1, Column: 6, Byte: 5},
    73  					End:      hcl.Pos{Line: 2, Column: 12, Byte: 33},
    74  				},
    75  			},
    76  			&Diagnostic{
    77  				Severity: "error",
    78  				Summary:  "Bad news",
    79  				Detail:   "It went wrong",
    80  				Range: &DiagnosticRange{
    81  					Filename: "modules/oops/missing.tf",
    82  					Start: Pos{
    83  						Line:   1,
    84  						Column: 6,
    85  						Byte:   5,
    86  					},
    87  					End: Pos{
    88  						Line:   2,
    89  						Column: 12,
    90  						Byte:   33,
    91  					},
    92  				},
    93  			},
    94  		},
    95  		"error with source code subject": {
    96  			&hcl.Diagnostic{
    97  				Severity: hcl.DiagError,
    98  				Summary:  "Tiny explosion",
    99  				Detail:   "Unexpected detonation while parsing",
   100  				Subject: &hcl.Range{
   101  					Filename: "test.tf",
   102  					Start:    hcl.Pos{Line: 1, Column: 10, Byte: 9},
   103  					End:      hcl.Pos{Line: 1, Column: 25, Byte: 24},
   104  				},
   105  			},
   106  			&Diagnostic{
   107  				Severity: "error",
   108  				Summary:  "Tiny explosion",
   109  				Detail:   "Unexpected detonation while parsing",
   110  				Range: &DiagnosticRange{
   111  					Filename: "test.tf",
   112  					Start: Pos{
   113  						Line:   1,
   114  						Column: 10,
   115  						Byte:   9,
   116  					},
   117  					End: Pos{
   118  						Line:   1,
   119  						Column: 25,
   120  						Byte:   24,
   121  					},
   122  				},
   123  				Snippet: &DiagnosticSnippet{
   124  					Context:              strPtr(`resource "test_resource" "test"`),
   125  					Code:                 `resource "test_resource" "test" {`,
   126  					StartLine:            1,
   127  					HighlightStartOffset: 9,
   128  					HighlightEndOffset:   24,
   129  					Values:               []DiagnosticExpressionValue{},
   130  				},
   131  			},
   132  		},
   133  		"error with source code subject but no context": {
   134  			&hcl.Diagnostic{
   135  				Severity: hcl.DiagError,
   136  				Summary:  "Nonsense input",
   137  				Detail:   "What you wrote makes no sense",
   138  				Subject: &hcl.Range{
   139  					Filename: "short.tf",
   140  					Start:    hcl.Pos{Line: 1, Column: 5, Byte: 4},
   141  					End:      hcl.Pos{Line: 1, Column: 10, Byte: 9},
   142  				},
   143  			},
   144  			&Diagnostic{
   145  				Severity: "error",
   146  				Summary:  "Nonsense input",
   147  				Detail:   "What you wrote makes no sense",
   148  				Range: &DiagnosticRange{
   149  					Filename: "short.tf",
   150  					Start: Pos{
   151  						Line:   1,
   152  						Column: 5,
   153  						Byte:   4,
   154  					},
   155  					End: Pos{
   156  						Line:   1,
   157  						Column: 10,
   158  						Byte:   9,
   159  					},
   160  				},
   161  				Snippet: &DiagnosticSnippet{
   162  					Context:              nil,
   163  					Code:                 (`bad source code`),
   164  					StartLine:            (1),
   165  					HighlightStartOffset: (4),
   166  					HighlightEndOffset:   (9),
   167  					Values:               []DiagnosticExpressionValue{},
   168  				},
   169  			},
   170  		},
   171  		"error with multi-line snippet": {
   172  			&hcl.Diagnostic{
   173  				Severity: hcl.DiagError,
   174  				Summary:  "In this house we respect booleans",
   175  				Detail:   "True or false, there is no maybe",
   176  				Subject: &hcl.Range{
   177  					Filename: "test.tf",
   178  					Start:    hcl.Pos{Line: 4, Column: 11, Byte: 81},
   179  					End:      hcl.Pos{Line: 4, Column: 16, Byte: 86},
   180  				},
   181  				Context: &hcl.Range{
   182  					Filename: "test.tf",
   183  					Start:    hcl.Pos{Line: 3, Column: 3, Byte: 63},
   184  					End:      hcl.Pos{Line: 5, Column: 4, Byte: 90},
   185  				},
   186  			},
   187  			&Diagnostic{
   188  				Severity: "error",
   189  				Summary:  "In this house we respect booleans",
   190  				Detail:   "True or false, there is no maybe",
   191  				Range: &DiagnosticRange{
   192  					Filename: "test.tf",
   193  					Start: Pos{
   194  						Line:   4,
   195  						Column: 11,
   196  						Byte:   81,
   197  					},
   198  					End: Pos{
   199  						Line:   4,
   200  						Column: 16,
   201  						Byte:   86,
   202  					},
   203  				},
   204  				Snippet: &DiagnosticSnippet{
   205  					Context:              strPtr(`resource "test_resource" "test"`),
   206  					Code:                 "  bar = {\n    baz = maybe\n  }",
   207  					StartLine:            3,
   208  					HighlightStartOffset: 20,
   209  					HighlightEndOffset:   25,
   210  					Values:               []DiagnosticExpressionValue{},
   211  				},
   212  			},
   213  		},
   214  		"error with empty highlight range at end of source code": {
   215  			&hcl.Diagnostic{
   216  				Severity: hcl.DiagError,
   217  				Summary:  "You forgot something",
   218  				Detail:   "Please finish your thought",
   219  				Subject: &hcl.Range{
   220  					Filename: "short.tf",
   221  					Start:    hcl.Pos{Line: 1, Column: 16, Byte: 15},
   222  					End:      hcl.Pos{Line: 1, Column: 16, Byte: 15},
   223  				},
   224  			},
   225  			&Diagnostic{
   226  				Severity: "error",
   227  				Summary:  "You forgot something",
   228  				Detail:   "Please finish your thought",
   229  				Range: &DiagnosticRange{
   230  					Filename: "short.tf",
   231  					Start: Pos{
   232  						Line:   1,
   233  						Column: 16,
   234  						Byte:   15,
   235  					},
   236  					End: Pos{
   237  						Line:   1,
   238  						Column: 17,
   239  						Byte:   16,
   240  					},
   241  				},
   242  				Snippet: &DiagnosticSnippet{
   243  					Code:                 ("bad source code"),
   244  					StartLine:            (1),
   245  					HighlightStartOffset: (15),
   246  					HighlightEndOffset:   (15),
   247  					Values:               []DiagnosticExpressionValue{},
   248  				},
   249  			},
   250  		},
   251  		"error with unset highlight end position": {
   252  			&hcl.Diagnostic{
   253  				Severity: hcl.DiagError,
   254  				Summary:  "There is no end",
   255  				Detail:   "But there is a beginning",
   256  				Subject: &hcl.Range{
   257  					Filename: "test.tf",
   258  					Start:    hcl.Pos{Line: 1, Column: 16, Byte: 15},
   259  					End:      hcl.Pos{Line: 0, Column: 0, Byte: 0},
   260  				},
   261  			},
   262  			&Diagnostic{
   263  				Severity: "error",
   264  				Summary:  "There is no end",
   265  				Detail:   "But there is a beginning",
   266  				Range: &DiagnosticRange{
   267  					Filename: "test.tf",
   268  					Start: Pos{
   269  						Line:   1,
   270  						Column: 16,
   271  						Byte:   15,
   272  					},
   273  					End: Pos{
   274  						Line:   1,
   275  						Column: 17,
   276  						Byte:   16,
   277  					},
   278  				},
   279  				Snippet: &DiagnosticSnippet{
   280  					Context:              strPtr(`resource "test_resource" "test"`),
   281  					Code:                 `resource "test_resource" "test" {`,
   282  					StartLine:            1,
   283  					HighlightStartOffset: 15,
   284  					HighlightEndOffset:   16,
   285  					Values:               []DiagnosticExpressionValue{},
   286  				},
   287  			},
   288  		},
   289  		"error whose range starts at a newline": {
   290  			&hcl.Diagnostic{
   291  				Severity: hcl.DiagError,
   292  				Summary:  "Invalid newline",
   293  				Detail:   "How awkward!",
   294  				Subject: &hcl.Range{
   295  					Filename: "odd-comment.tf",
   296  					Start:    hcl.Pos{Line: 2, Column: 5, Byte: 4},
   297  					End:      hcl.Pos{Line: 3, Column: 1, Byte: 6},
   298  				},
   299  			},
   300  			&Diagnostic{
   301  				Severity: "error",
   302  				Summary:  "Invalid newline",
   303  				Detail:   "How awkward!",
   304  				Range: &DiagnosticRange{
   305  					Filename: "odd-comment.tf",
   306  					Start: Pos{
   307  						Line:   2,
   308  						Column: 5,
   309  						Byte:   4,
   310  					},
   311  					End: Pos{
   312  						Line:   3,
   313  						Column: 1,
   314  						Byte:   6,
   315  					},
   316  				},
   317  				Snippet: &DiagnosticSnippet{
   318  					Code:      `#`,
   319  					StartLine: 2,
   320  					Values:    []DiagnosticExpressionValue{},
   321  
   322  					// Due to the range starting at a newline on a blank
   323  					// line, we end up stripping off the initial newline
   324  					// to produce only a one-line snippet. That would
   325  					// therefore cause the start offset to naturally be
   326  					// -1, just before the Code we returned, but then we
   327  					// force it to zero so that the result will still be
   328  					// in range for a byte-oriented slice of Code.
   329  					HighlightStartOffset: 0,
   330  					HighlightEndOffset:   1,
   331  				},
   332  			},
   333  		},
   334  		"error with source code subject and known expression": {
   335  			&hcl.Diagnostic{
   336  				Severity: hcl.DiagError,
   337  				Summary:  "Wrong noises",
   338  				Detail:   "Biological sounds are not allowed",
   339  				Subject: &hcl.Range{
   340  					Filename: "test.tf",
   341  					Start:    hcl.Pos{Line: 2, Column: 9, Byte: 42},
   342  					End:      hcl.Pos{Line: 2, Column: 26, Byte: 59},
   343  				},
   344  				Expression: hcltest.MockExprTraversal(hcl.Traversal{
   345  					hcl.TraverseRoot{Name: "var"},
   346  					hcl.TraverseAttr{Name: "boop"},
   347  					hcl.TraverseIndex{Key: cty.StringVal("hello!")},
   348  				}),
   349  				EvalContext: &hcl.EvalContext{
   350  					Variables: map[string]cty.Value{
   351  						"var": cty.ObjectVal(map[string]cty.Value{
   352  							"boop": cty.MapVal(map[string]cty.Value{
   353  								"hello!": cty.StringVal("bleurgh"),
   354  							}),
   355  						}),
   356  					},
   357  				},
   358  			},
   359  			&Diagnostic{
   360  				Severity: "error",
   361  				Summary:  "Wrong noises",
   362  				Detail:   "Biological sounds are not allowed",
   363  				Range: &DiagnosticRange{
   364  					Filename: "test.tf",
   365  					Start: Pos{
   366  						Line:   2,
   367  						Column: 9,
   368  						Byte:   42,
   369  					},
   370  					End: Pos{
   371  						Line:   2,
   372  						Column: 26,
   373  						Byte:   59,
   374  					},
   375  				},
   376  				Snippet: &DiagnosticSnippet{
   377  					Context:              strPtr(`resource "test_resource" "test"`),
   378  					Code:                 (`  foo = var.boop["hello!"]`),
   379  					StartLine:            (2),
   380  					HighlightStartOffset: (8),
   381  					HighlightEndOffset:   (25),
   382  					Values: []DiagnosticExpressionValue{
   383  						{
   384  							Traversal: `var.boop["hello!"]`,
   385  							Statement: `is "bleurgh"`,
   386  						},
   387  					},
   388  				},
   389  			},
   390  		},
   391  		"error with source code subject and expression referring to sensitive value": {
   392  			&hcl.Diagnostic{
   393  				Severity: hcl.DiagError,
   394  				Summary:  "Wrong noises",
   395  				Detail:   "Biological sounds are not allowed",
   396  				Subject: &hcl.Range{
   397  					Filename: "test.tf",
   398  					Start:    hcl.Pos{Line: 2, Column: 9, Byte: 42},
   399  					End:      hcl.Pos{Line: 2, Column: 26, Byte: 59},
   400  				},
   401  				Expression: hcltest.MockExprTraversal(hcl.Traversal{
   402  					hcl.TraverseRoot{Name: "var"},
   403  					hcl.TraverseAttr{Name: "boop"},
   404  					hcl.TraverseIndex{Key: cty.StringVal("hello!")},
   405  				}),
   406  				EvalContext: &hcl.EvalContext{
   407  					Variables: map[string]cty.Value{
   408  						"var": cty.ObjectVal(map[string]cty.Value{
   409  							"boop": cty.MapVal(map[string]cty.Value{
   410  								"hello!": cty.StringVal("bleurgh").Mark(marks.Sensitive),
   411  							}),
   412  						}),
   413  					},
   414  				},
   415  				Extra: diagnosticCausedBySensitive(true),
   416  			},
   417  			&Diagnostic{
   418  				Severity: "error",
   419  				Summary:  "Wrong noises",
   420  				Detail:   "Biological sounds are not allowed",
   421  				Range: &DiagnosticRange{
   422  					Filename: "test.tf",
   423  					Start: Pos{
   424  						Line:   2,
   425  						Column: 9,
   426  						Byte:   42,
   427  					},
   428  					End: Pos{
   429  						Line:   2,
   430  						Column: 26,
   431  						Byte:   59,
   432  					},
   433  				},
   434  				Snippet: &DiagnosticSnippet{
   435  					Context:              strPtr(`resource "test_resource" "test"`),
   436  					Code:                 (`  foo = var.boop["hello!"]`),
   437  					StartLine:            (2),
   438  					HighlightStartOffset: (8),
   439  					HighlightEndOffset:   (25),
   440  					Values: []DiagnosticExpressionValue{
   441  						{
   442  							Traversal: `var.boop["hello!"]`,
   443  							Statement: `has a sensitive value`,
   444  						},
   445  					},
   446  				},
   447  			},
   448  		},
   449  		"error with source code subject and expression referring to sensitive value when not caused by sensitive values": {
   450  			&hcl.Diagnostic{
   451  				Severity: hcl.DiagError,
   452  				Summary:  "Wrong noises",
   453  				Detail:   "Biological sounds are not allowed",
   454  				Subject: &hcl.Range{
   455  					Filename: "test.tf",
   456  					Start:    hcl.Pos{Line: 2, Column: 9, Byte: 42},
   457  					End:      hcl.Pos{Line: 2, Column: 26, Byte: 59},
   458  				},
   459  				Expression: hcltest.MockExprTraversal(hcl.Traversal{
   460  					hcl.TraverseRoot{Name: "var"},
   461  					hcl.TraverseAttr{Name: "boop"},
   462  					hcl.TraverseIndex{Key: cty.StringVal("hello!")},
   463  				}),
   464  				EvalContext: &hcl.EvalContext{
   465  					Variables: map[string]cty.Value{
   466  						"var": cty.ObjectVal(map[string]cty.Value{
   467  							"boop": cty.MapVal(map[string]cty.Value{
   468  								"hello!": cty.StringVal("bleurgh").Mark(marks.Sensitive),
   469  							}),
   470  						}),
   471  					},
   472  				},
   473  			},
   474  			&Diagnostic{
   475  				Severity: "error",
   476  				Summary:  "Wrong noises",
   477  				Detail:   "Biological sounds are not allowed",
   478  				Range: &DiagnosticRange{
   479  					Filename: "test.tf",
   480  					Start: Pos{
   481  						Line:   2,
   482  						Column: 9,
   483  						Byte:   42,
   484  					},
   485  					End: Pos{
   486  						Line:   2,
   487  						Column: 26,
   488  						Byte:   59,
   489  					},
   490  				},
   491  				Snippet: &DiagnosticSnippet{
   492  					Context:              strPtr(`resource "test_resource" "test"`),
   493  					Code:                 (`  foo = var.boop["hello!"]`),
   494  					StartLine:            (2),
   495  					HighlightStartOffset: (8),
   496  					HighlightEndOffset:   (25),
   497  					Values:               []DiagnosticExpressionValue{
   498  						// The sensitive value is filtered out because this is
   499  						// not a sensitive-value-related diagnostic message.
   500  					},
   501  				},
   502  			},
   503  		},
   504  		"error with source code subject and expression referring to a collection containing a sensitive value": {
   505  			&hcl.Diagnostic{
   506  				Severity: hcl.DiagError,
   507  				Summary:  "Wrong noises",
   508  				Detail:   "Biological sounds are not allowed",
   509  				Subject: &hcl.Range{
   510  					Filename: "test.tf",
   511  					Start:    hcl.Pos{Line: 2, Column: 9, Byte: 42},
   512  					End:      hcl.Pos{Line: 2, Column: 26, Byte: 59},
   513  				},
   514  				Expression: hcltest.MockExprTraversal(hcl.Traversal{
   515  					hcl.TraverseRoot{Name: "var"},
   516  					hcl.TraverseAttr{Name: "boop"},
   517  				}),
   518  				EvalContext: &hcl.EvalContext{
   519  					Variables: map[string]cty.Value{
   520  						"var": cty.ObjectVal(map[string]cty.Value{
   521  							"boop": cty.MapVal(map[string]cty.Value{
   522  								"hello!": cty.StringVal("bleurgh").Mark(marks.Sensitive),
   523  							}),
   524  						}),
   525  					},
   526  				},
   527  			},
   528  			&Diagnostic{
   529  				Severity: "error",
   530  				Summary:  "Wrong noises",
   531  				Detail:   "Biological sounds are not allowed",
   532  				Range: &DiagnosticRange{
   533  					Filename: "test.tf",
   534  					Start: Pos{
   535  						Line:   2,
   536  						Column: 9,
   537  						Byte:   42,
   538  					},
   539  					End: Pos{
   540  						Line:   2,
   541  						Column: 26,
   542  						Byte:   59,
   543  					},
   544  				},
   545  				Snippet: &DiagnosticSnippet{
   546  					Context:              strPtr(`resource "test_resource" "test"`),
   547  					Code:                 (`  foo = var.boop["hello!"]`),
   548  					StartLine:            (2),
   549  					HighlightStartOffset: (8),
   550  					HighlightEndOffset:   (25),
   551  					Values: []DiagnosticExpressionValue{
   552  						{
   553  							Traversal: `var.boop`,
   554  							Statement: `is map of string with 1 element`,
   555  						},
   556  					},
   557  				},
   558  			},
   559  		},
   560  		"error with source code subject and unknown string expression": {
   561  			&hcl.Diagnostic{
   562  				Severity: hcl.DiagError,
   563  				Summary:  "Wrong noises",
   564  				Detail:   "Biological sounds are not allowed",
   565  				Subject: &hcl.Range{
   566  					Filename: "test.tf",
   567  					Start:    hcl.Pos{Line: 2, Column: 9, Byte: 42},
   568  					End:      hcl.Pos{Line: 2, Column: 26, Byte: 59},
   569  				},
   570  				Expression: hcltest.MockExprTraversal(hcl.Traversal{
   571  					hcl.TraverseRoot{Name: "var"},
   572  					hcl.TraverseAttr{Name: "boop"},
   573  					hcl.TraverseIndex{Key: cty.StringVal("hello!")},
   574  				}),
   575  				EvalContext: &hcl.EvalContext{
   576  					Variables: map[string]cty.Value{
   577  						"var": cty.ObjectVal(map[string]cty.Value{
   578  							"boop": cty.MapVal(map[string]cty.Value{
   579  								"hello!": cty.UnknownVal(cty.String),
   580  							}),
   581  						}),
   582  					},
   583  				},
   584  				Extra: diagnosticCausedByUnknown(true),
   585  			},
   586  			&Diagnostic{
   587  				Severity: "error",
   588  				Summary:  "Wrong noises",
   589  				Detail:   "Biological sounds are not allowed",
   590  				Range: &DiagnosticRange{
   591  					Filename: "test.tf",
   592  					Start: Pos{
   593  						Line:   2,
   594  						Column: 9,
   595  						Byte:   42,
   596  					},
   597  					End: Pos{
   598  						Line:   2,
   599  						Column: 26,
   600  						Byte:   59,
   601  					},
   602  				},
   603  				Snippet: &DiagnosticSnippet{
   604  					Context:              strPtr(`resource "test_resource" "test"`),
   605  					Code:                 (`  foo = var.boop["hello!"]`),
   606  					StartLine:            (2),
   607  					HighlightStartOffset: (8),
   608  					HighlightEndOffset:   (25),
   609  					Values: []DiagnosticExpressionValue{
   610  						{
   611  							Traversal: `var.boop["hello!"]`,
   612  							Statement: `is a string, known only after apply`,
   613  						},
   614  					},
   615  				},
   616  			},
   617  		},
   618  		"error with source code subject and unknown expression of unknown type": {
   619  			&hcl.Diagnostic{
   620  				Severity: hcl.DiagError,
   621  				Summary:  "Wrong noises",
   622  				Detail:   "Biological sounds are not allowed",
   623  				Subject: &hcl.Range{
   624  					Filename: "test.tf",
   625  					Start:    hcl.Pos{Line: 2, Column: 9, Byte: 42},
   626  					End:      hcl.Pos{Line: 2, Column: 26, Byte: 59},
   627  				},
   628  				Expression: hcltest.MockExprTraversal(hcl.Traversal{
   629  					hcl.TraverseRoot{Name: "var"},
   630  					hcl.TraverseAttr{Name: "boop"},
   631  					hcl.TraverseIndex{Key: cty.StringVal("hello!")},
   632  				}),
   633  				EvalContext: &hcl.EvalContext{
   634  					Variables: map[string]cty.Value{
   635  						"var": cty.ObjectVal(map[string]cty.Value{
   636  							"boop": cty.MapVal(map[string]cty.Value{
   637  								"hello!": cty.UnknownVal(cty.DynamicPseudoType),
   638  							}),
   639  						}),
   640  					},
   641  				},
   642  				Extra: diagnosticCausedByUnknown(true),
   643  			},
   644  			&Diagnostic{
   645  				Severity: "error",
   646  				Summary:  "Wrong noises",
   647  				Detail:   "Biological sounds are not allowed",
   648  				Range: &DiagnosticRange{
   649  					Filename: "test.tf",
   650  					Start: Pos{
   651  						Line:   2,
   652  						Column: 9,
   653  						Byte:   42,
   654  					},
   655  					End: Pos{
   656  						Line:   2,
   657  						Column: 26,
   658  						Byte:   59,
   659  					},
   660  				},
   661  				Snippet: &DiagnosticSnippet{
   662  					Context:              strPtr(`resource "test_resource" "test"`),
   663  					Code:                 (`  foo = var.boop["hello!"]`),
   664  					StartLine:            (2),
   665  					HighlightStartOffset: (8),
   666  					HighlightEndOffset:   (25),
   667  					Values: []DiagnosticExpressionValue{
   668  						{
   669  							Traversal: `var.boop["hello!"]`,
   670  							Statement: `will be known only after apply`,
   671  						},
   672  					},
   673  				},
   674  			},
   675  		},
   676  		"error with source code subject and unknown expression of unknown type when not caused by unknown values": {
   677  			&hcl.Diagnostic{
   678  				Severity: hcl.DiagError,
   679  				Summary:  "Wrong noises",
   680  				Detail:   "Biological sounds are not allowed",
   681  				Subject: &hcl.Range{
   682  					Filename: "test.tf",
   683  					Start:    hcl.Pos{Line: 2, Column: 9, Byte: 42},
   684  					End:      hcl.Pos{Line: 2, Column: 26, Byte: 59},
   685  				},
   686  				Expression: hcltest.MockExprTraversal(hcl.Traversal{
   687  					hcl.TraverseRoot{Name: "var"},
   688  					hcl.TraverseAttr{Name: "boop"},
   689  					hcl.TraverseIndex{Key: cty.StringVal("hello!")},
   690  				}),
   691  				EvalContext: &hcl.EvalContext{
   692  					Variables: map[string]cty.Value{
   693  						"var": cty.ObjectVal(map[string]cty.Value{
   694  							"boop": cty.MapVal(map[string]cty.Value{
   695  								"hello!": cty.UnknownVal(cty.DynamicPseudoType),
   696  							}),
   697  						}),
   698  					},
   699  				},
   700  			},
   701  			&Diagnostic{
   702  				Severity: "error",
   703  				Summary:  "Wrong noises",
   704  				Detail:   "Biological sounds are not allowed",
   705  				Range: &DiagnosticRange{
   706  					Filename: "test.tf",
   707  					Start: Pos{
   708  						Line:   2,
   709  						Column: 9,
   710  						Byte:   42,
   711  					},
   712  					End: Pos{
   713  						Line:   2,
   714  						Column: 26,
   715  						Byte:   59,
   716  					},
   717  				},
   718  				Snippet: &DiagnosticSnippet{
   719  					Context:              strPtr(`resource "test_resource" "test"`),
   720  					Code:                 (`  foo = var.boop["hello!"]`),
   721  					StartLine:            (2),
   722  					HighlightStartOffset: (8),
   723  					HighlightEndOffset:   (25),
   724  					Values:               []DiagnosticExpressionValue{
   725  						// The unknown value is filtered out because this is
   726  						// not an unknown-value-related diagnostic message.
   727  					},
   728  				},
   729  			},
   730  		},
   731  		"error with source code subject with multiple expression values": {
   732  			&hcl.Diagnostic{
   733  				Severity: hcl.DiagError,
   734  				Summary:  "Catastrophic failure",
   735  				Detail:   "Basically, everything went wrong",
   736  				Subject: &hcl.Range{
   737  					Filename: "values.tf",
   738  					Start:    hcl.Pos{Line: 1, Column: 1, Byte: 0},
   739  					End:      hcl.Pos{Line: 13, Column: 2, Byte: 102},
   740  				},
   741  				Expression: hcltest.MockExprList([]hcl.Expression{
   742  					hcltest.MockExprTraversalSrc("var.a"),
   743  					hcltest.MockExprTraversalSrc("var.b"),
   744  					hcltest.MockExprTraversalSrc("var.c"),
   745  					hcltest.MockExprTraversalSrc("var.d"),
   746  					hcltest.MockExprTraversalSrc("var.e"),
   747  					hcltest.MockExprTraversalSrc("var.f"),
   748  					hcltest.MockExprTraversalSrc("var.g"),
   749  					hcltest.MockExprTraversalSrc("var.h"),
   750  					hcltest.MockExprTraversalSrc("var.i"),
   751  					hcltest.MockExprTraversalSrc("var.j"),
   752  					hcltest.MockExprTraversalSrc("var.k"),
   753  				}),
   754  				EvalContext: &hcl.EvalContext{
   755  					Variables: map[string]cty.Value{
   756  						"var": cty.ObjectVal(map[string]cty.Value{
   757  							"a": cty.True,
   758  							"b": cty.NumberFloatVal(123.45),
   759  							"c": cty.NullVal(cty.String),
   760  							"d": cty.StringVal("secret").Mark(marks.Sensitive),
   761  							"e": cty.False,
   762  							"f": cty.ListValEmpty(cty.String),
   763  							"g": cty.MapVal(map[string]cty.Value{
   764  								"boop": cty.StringVal("beep"),
   765  							}),
   766  							"h": cty.ListVal([]cty.Value{
   767  								cty.StringVal("boop"),
   768  								cty.StringVal("beep"),
   769  								cty.StringVal("blorp"),
   770  							}),
   771  							"i": cty.EmptyObjectVal,
   772  							"j": cty.ObjectVal(map[string]cty.Value{
   773  								"foo": cty.StringVal("bar"),
   774  							}),
   775  							"k": cty.ObjectVal(map[string]cty.Value{
   776  								"a": cty.True,
   777  								"b": cty.False,
   778  							}),
   779  						}),
   780  					},
   781  				},
   782  				Extra: diagnosticCausedBySensitive(true),
   783  			},
   784  			&Diagnostic{
   785  				Severity: "error",
   786  				Summary:  "Catastrophic failure",
   787  				Detail:   "Basically, everything went wrong",
   788  				Range: &DiagnosticRange{
   789  					Filename: "values.tf",
   790  					Start: Pos{
   791  						Line:   1,
   792  						Column: 1,
   793  						Byte:   0,
   794  					},
   795  					End: Pos{
   796  						Line:   13,
   797  						Column: 2,
   798  						Byte:   102,
   799  					},
   800  				},
   801  				Snippet: &DiagnosticSnippet{
   802  					Code: `[
   803    var.a,
   804    var.b,
   805    var.c,
   806    var.d,
   807    var.e,
   808    var.f,
   809    var.g,
   810    var.h,
   811    var.i,
   812    var.j,
   813    var.k,
   814  ]`,
   815  					StartLine:            (1),
   816  					HighlightStartOffset: (0),
   817  					HighlightEndOffset:   (102),
   818  					Values: []DiagnosticExpressionValue{
   819  						{
   820  							Traversal: `var.a`,
   821  							Statement: `is true`,
   822  						},
   823  						{
   824  							Traversal: `var.b`,
   825  							Statement: `is 123.45`,
   826  						},
   827  						{
   828  							Traversal: `var.c`,
   829  							Statement: `is null`,
   830  						},
   831  						{
   832  							Traversal: `var.d`,
   833  							Statement: `has a sensitive value`,
   834  						},
   835  						{
   836  							Traversal: `var.e`,
   837  							Statement: `is false`,
   838  						},
   839  						{
   840  							Traversal: `var.f`,
   841  							Statement: `is empty list of string`,
   842  						},
   843  						{
   844  							Traversal: `var.g`,
   845  							Statement: `is map of string with 1 element`,
   846  						},
   847  						{
   848  							Traversal: `var.h`,
   849  							Statement: `is list of string with 3 elements`,
   850  						},
   851  						{
   852  							Traversal: `var.i`,
   853  							Statement: `is object with no attributes`,
   854  						},
   855  						{
   856  							Traversal: `var.j`,
   857  							Statement: `is object with 1 attribute "foo"`,
   858  						},
   859  						{
   860  							Traversal: `var.k`,
   861  							Statement: `is object with 2 attributes`,
   862  						},
   863  					},
   864  				},
   865  			},
   866  		},
   867  	}
   868  
   869  	for name, tc := range testCases {
   870  		t.Run(name, func(t *testing.T) {
   871  			// Convert the diag into a tfdiags.Diagnostic
   872  			var diags tfdiags.Diagnostics
   873  			diags = diags.Append(tc.diag)
   874  
   875  			got := NewDiagnostic(diags[0], sources)
   876  			if !cmp.Equal(tc.want, got) {
   877  				t.Fatalf("wrong result\n:%s", cmp.Diff(tc.want, got))
   878  			}
   879  		})
   880  
   881  		t.Run(fmt.Sprintf("golden test for %s", name), func(t *testing.T) {
   882  			// Convert the diag into a tfdiags.Diagnostic
   883  			var diags tfdiags.Diagnostics
   884  			diags = diags.Append(tc.diag)
   885  
   886  			got := NewDiagnostic(diags[0], sources)
   887  
   888  			// Render the diagnostic to indented JSON
   889  			gotBytes, err := json.MarshalIndent(got, "", "  ")
   890  			if err != nil {
   891  				t.Fatal(err)
   892  			}
   893  
   894  			// Compare against the golden reference
   895  			filename := path.Join(
   896  				"testdata",
   897  				"diagnostic",
   898  				fmt.Sprintf("%s.json", strings.ReplaceAll(name, " ", "-")),
   899  			)
   900  
   901  			// Generate golden reference by uncommenting the next two lines:
   902  			// gotBytes = append(gotBytes, '\n')
   903  			// os.WriteFile(filename, gotBytes, 0644)
   904  
   905  			wantFile, err := os.Open(filename)
   906  			if err != nil {
   907  				t.Fatalf("failed to open golden file: %s", err)
   908  			}
   909  			defer wantFile.Close()
   910  			wantBytes, err := ioutil.ReadAll(wantFile)
   911  			if err != nil {
   912  				t.Fatalf("failed to read output file: %s", err)
   913  			}
   914  
   915  			// Don't care about leading or trailing whitespace
   916  			gotString := strings.TrimSpace(string(gotBytes))
   917  			wantString := strings.TrimSpace(string(wantBytes))
   918  
   919  			if !cmp.Equal(wantString, gotString) {
   920  				t.Fatalf("wrong result\n:%s", cmp.Diff(wantString, gotString))
   921  			}
   922  		})
   923  	}
   924  }
   925  
   926  // Helper function to make constructing literal Diagnostics easier. There
   927  // are fields which are pointer-to-string to ensure that the rendered JSON
   928  // results in `null` for an empty value, rather than `""`.
   929  func strPtr(s string) *string { return &s }
   930  
   931  // diagnosticCausedByUnknown is a testing helper for exercising our logic
   932  // for selectively showing unknown values alongside our source snippets for
   933  // diagnostics that are explicitly marked as being caused by unknown values.
   934  type diagnosticCausedByUnknown bool
   935  
   936  var _ tfdiags.DiagnosticExtraBecauseUnknown = diagnosticCausedByUnknown(true)
   937  
   938  func (e diagnosticCausedByUnknown) DiagnosticCausedByUnknown() bool {
   939  	return bool(e)
   940  }
   941  
   942  // diagnosticCausedBySensitive is a testing helper for exercising our logic
   943  // for selectively showing sensitive values alongside our source snippets for
   944  // diagnostics that are explicitly marked as being caused by sensitive values.
   945  type diagnosticCausedBySensitive bool
   946  
   947  var _ tfdiags.DiagnosticExtraBecauseSensitive = diagnosticCausedBySensitive(true)
   948  
   949  func (e diagnosticCausedBySensitive) DiagnosticCausedBySensitive() bool {
   950  	return bool(e)
   951  }