github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/command/views/json/diagnostic_test.go (about)

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