github.com/opentofu/opentofu@v1.7.1/internal/command/views/json/diagnostic_test.go (about)

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