package tfdiags import ( "fmt" "reflect" "testing" "github.com/go-test/deep" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" "github.com/zclconf/go-cty/cty" ) func TestAttributeValue(t *testing.T) { testConfig := ` foo { bar = "hi" } foo { bar = "bar" } bar { bar = "woot" } baz "a" { bar = "beep" } baz "b" { bar = "boop" } parent { nested_str = "hello" nested_str_tuple = ["aa", "bbb", "cccc"] nested_num_tuple = [1, 9863, 22] nested_map = { first_key = "first_value" second_key = "2nd value" } } tuple_of_one = ["one"] tuple_of_two = ["first", "22222"] root_map = { first = "1st" second = "2nd" } simple_attr = "val" ` // TODO: Test ConditionalExpr // TODO: Test ForExpr // TODO: Test FunctionCallExpr // TODO: Test IndexExpr // TODO: Test interpolation // TODO: Test SplatExpr f, parseDiags := hclsyntax.ParseConfig([]byte(testConfig), "test.tf", hcl.Pos{Line: 1, Column: 1}) if len(parseDiags) != 0 { t.Fatal(parseDiags) } emptySrcRng := &SourceRange{ Filename: "test.tf", Start: SourcePos{Line: 1, Column: 1, Byte: 0}, End: SourcePos{Line: 1, Column: 1, Byte: 0}, } testCases := []struct { Diag Diagnostic ExpectedRange *SourceRange }{ { AttributeValue( Error, "foo[0].bar", "detail", cty.Path{ cty.GetAttrStep{Name: "foo"}, cty.IndexStep{Key: cty.NumberIntVal(0)}, cty.GetAttrStep{Name: "bar"}, }, ), &SourceRange{ Filename: "test.tf", Start: SourcePos{Line: 3, Column: 9, Byte: 15}, End: SourcePos{Line: 3, Column: 13, Byte: 19}, }, }, { AttributeValue( Error, "foo[1].bar", "detail", cty.Path{ cty.GetAttrStep{Name: "foo"}, cty.IndexStep{Key: cty.NumberIntVal(1)}, cty.GetAttrStep{Name: "bar"}, }, ), &SourceRange{ Filename: "test.tf", Start: SourcePos{Line: 6, Column: 9, Byte: 36}, End: SourcePos{Line: 6, Column: 14, Byte: 41}, }, }, { AttributeValue( Error, "foo[99].bar", "detail", cty.Path{ cty.GetAttrStep{Name: "foo"}, cty.IndexStep{Key: cty.NumberIntVal(99)}, cty.GetAttrStep{Name: "bar"}, }, ), emptySrcRng, }, { AttributeValue( Error, "bar.bar", "detail", cty.Path{ cty.GetAttrStep{Name: "bar"}, cty.GetAttrStep{Name: "bar"}, }, ), &SourceRange{ Filename: "test.tf", Start: SourcePos{Line: 9, Column: 9, Byte: 58}, End: SourcePos{Line: 9, Column: 15, Byte: 64}, }, }, { AttributeValue( Error, `baz["a"].bar`, "detail", cty.Path{ cty.GetAttrStep{Name: "baz"}, cty.IndexStep{Key: cty.StringVal("a")}, cty.GetAttrStep{Name: "bar"}, }, ), &SourceRange{ Filename: "test.tf", Start: SourcePos{Line: 12, Column: 9, Byte: 85}, End: SourcePos{Line: 12, Column: 15, Byte: 91}, }, }, { AttributeValue( Error, `baz["b"].bar`, "detail", cty.Path{ cty.GetAttrStep{Name: "baz"}, cty.IndexStep{Key: cty.StringVal("b")}, cty.GetAttrStep{Name: "bar"}, }, ), &SourceRange{ Filename: "test.tf", Start: SourcePos{Line: 15, Column: 9, Byte: 112}, End: SourcePos{Line: 15, Column: 15, Byte: 118}, }, }, { AttributeValue( Error, `baz["not_exists"].bar`, "detail", cty.Path{ cty.GetAttrStep{Name: "baz"}, cty.IndexStep{Key: cty.StringVal("not_exists")}, cty.GetAttrStep{Name: "bar"}, }, ), emptySrcRng, }, { // Attribute value with subject already populated should not be disturbed. // (in a real case, this might've been passed through from a deeper function // in the call stack, for example.) &attributeDiagnostic{ attrPath: cty.Path{cty.GetAttrStep{Name: "foo"}}, diagnosticBase: diagnosticBase{ summary: "preexisting", detail: "detail", address: "original", }, subject: &SourceRange{ Filename: "somewhere_else.tf", }, }, &SourceRange{ Filename: "somewhere_else.tf", }, }, { // Missing path &attributeDiagnostic{ diagnosticBase: diagnosticBase{ summary: "missing path", }, }, nil, }, // Nested attributes { AttributeValue( Error, "parent.nested_str", "detail", cty.Path{ cty.GetAttrStep{Name: "parent"}, cty.GetAttrStep{Name: "nested_str"}, }, ), &SourceRange{ Filename: "test.tf", Start: SourcePos{Line: 18, Column: 16, Byte: 145}, End: SourcePos{Line: 18, Column: 23, Byte: 152}, }, }, { AttributeValue( Error, "parent.nested_str_tuple[99]", "detail", cty.Path{ cty.GetAttrStep{Name: "parent"}, cty.GetAttrStep{Name: "nested_str_tuple"}, cty.IndexStep{Key: cty.NumberIntVal(99)}, }, ), &SourceRange{ Filename: "test.tf", Start: SourcePos{Line: 19, Column: 3, Byte: 155}, End: SourcePos{Line: 19, Column: 19, Byte: 171}, }, }, { AttributeValue( Error, "parent.nested_str_tuple[0]", "detail", cty.Path{ cty.GetAttrStep{Name: "parent"}, cty.GetAttrStep{Name: "nested_str_tuple"}, cty.IndexStep{Key: cty.NumberIntVal(0)}, }, ), &SourceRange{ Filename: "test.tf", Start: SourcePos{Line: 19, Column: 23, Byte: 175}, End: SourcePos{Line: 19, Column: 27, Byte: 179}, }, }, { AttributeValue( Error, "parent.nested_str_tuple[2]", "detail", cty.Path{ cty.GetAttrStep{Name: "parent"}, cty.GetAttrStep{Name: "nested_str_tuple"}, cty.IndexStep{Key: cty.NumberIntVal(2)}, }, ), &SourceRange{ Filename: "test.tf", Start: SourcePos{Line: 19, Column: 36, Byte: 188}, End: SourcePos{Line: 19, Column: 42, Byte: 194}, }, }, { AttributeValue( Error, "parent.nested_num_tuple[0]", "detail", cty.Path{ cty.GetAttrStep{Name: "parent"}, cty.GetAttrStep{Name: "nested_num_tuple"}, cty.IndexStep{Key: cty.NumberIntVal(0)}, }, ), &SourceRange{ Filename: "test.tf", Start: SourcePos{Line: 20, Column: 23, Byte: 218}, End: SourcePos{Line: 20, Column: 24, Byte: 219}, }, }, { AttributeValue( Error, "parent.nested_num_tuple[1]", "detail", cty.Path{ cty.GetAttrStep{Name: "parent"}, cty.GetAttrStep{Name: "nested_num_tuple"}, cty.IndexStep{Key: cty.NumberIntVal(1)}, }, ), &SourceRange{ Filename: "test.tf", Start: SourcePos{Line: 20, Column: 26, Byte: 221}, End: SourcePos{Line: 20, Column: 30, Byte: 225}, }, }, { AttributeValue( Error, "parent.nested_map.first_key", "detail", cty.Path{ cty.GetAttrStep{Name: "parent"}, cty.GetAttrStep{Name: "nested_map"}, cty.IndexStep{Key: cty.StringVal("first_key")}, }, ), &SourceRange{ Filename: "test.tf", Start: SourcePos{Line: 22, Column: 19, Byte: 266}, End: SourcePos{Line: 22, Column: 30, Byte: 277}, }, }, { AttributeValue( Error, "parent.nested_map.second_key", "detail", cty.Path{ cty.GetAttrStep{Name: "parent"}, cty.GetAttrStep{Name: "nested_map"}, cty.IndexStep{Key: cty.StringVal("second_key")}, }, ), &SourceRange{ Filename: "test.tf", Start: SourcePos{Line: 23, Column: 19, Byte: 297}, End: SourcePos{Line: 23, Column: 28, Byte: 306}, }, }, { AttributeValue( Error, "parent.nested_map.undefined_key", "detail", cty.Path{ cty.GetAttrStep{Name: "parent"}, cty.GetAttrStep{Name: "nested_map"}, cty.IndexStep{Key: cty.StringVal("undefined_key")}, }, ), &SourceRange{ Filename: "test.tf", Start: SourcePos{Line: 21, Column: 3, Byte: 233}, End: SourcePos{Line: 21, Column: 13, Byte: 243}, }, }, // Root attributes of complex types { AttributeValue( Error, "tuple_of_one[0]", "detail", cty.Path{ cty.GetAttrStep{Name: "tuple_of_one"}, cty.IndexStep{Key: cty.NumberIntVal(0)}, }, ), &SourceRange{ Filename: "test.tf", Start: SourcePos{Line: 26, Column: 17, Byte: 330}, End: SourcePos{Line: 26, Column: 22, Byte: 335}, }, }, { AttributeValue( Error, "tuple_of_two[0]", "detail", cty.Path{ cty.GetAttrStep{Name: "tuple_of_two"}, cty.IndexStep{Key: cty.NumberIntVal(0)}, }, ), &SourceRange{ Filename: "test.tf", Start: SourcePos{Line: 27, Column: 17, Byte: 353}, End: SourcePos{Line: 27, Column: 24, Byte: 360}, }, }, { AttributeValue( Error, "tuple_of_two[1]", "detail", cty.Path{ cty.GetAttrStep{Name: "tuple_of_two"}, cty.IndexStep{Key: cty.NumberIntVal(1)}, }, ), &SourceRange{ Filename: "test.tf", Start: SourcePos{Line: 27, Column: 26, Byte: 362}, End: SourcePos{Line: 27, Column: 33, Byte: 369}, }, }, { AttributeValue( Error, "tuple_of_one[null]", "detail", cty.Path{ cty.GetAttrStep{Name: "tuple_of_one"}, cty.IndexStep{Key: cty.NullVal(cty.Number)}, }, ), &SourceRange{ Filename: "test.tf", Start: SourcePos{Line: 26, Column: 1, Byte: 314}, End: SourcePos{Line: 26, Column: 13, Byte: 326}, }, }, { // index out of range AttributeValue( Error, "tuple_of_two[99]", "detail", cty.Path{ cty.GetAttrStep{Name: "tuple_of_two"}, cty.IndexStep{Key: cty.NumberIntVal(99)}, }, ), &SourceRange{ Filename: "test.tf", Start: SourcePos{Line: 27, Column: 1, Byte: 337}, End: SourcePos{Line: 27, Column: 13, Byte: 349}, }, }, { AttributeValue( Error, "root_map.first", "detail", cty.Path{ cty.GetAttrStep{Name: "root_map"}, cty.IndexStep{Key: cty.StringVal("first")}, }, ), &SourceRange{ Filename: "test.tf", Start: SourcePos{Line: 29, Column: 13, Byte: 396}, End: SourcePos{Line: 29, Column: 16, Byte: 399}, }, }, { AttributeValue( Error, "root_map.second", "detail", cty.Path{ cty.GetAttrStep{Name: "root_map"}, cty.IndexStep{Key: cty.StringVal("second")}, }, ), &SourceRange{ Filename: "test.tf", Start: SourcePos{Line: 30, Column: 13, Byte: 413}, End: SourcePos{Line: 30, Column: 16, Byte: 416}, }, }, { AttributeValue( Error, "root_map.undefined_key", "detail", cty.Path{ cty.GetAttrStep{Name: "root_map"}, cty.IndexStep{Key: cty.StringVal("undefined_key")}, }, ), &SourceRange{ Filename: "test.tf", Start: SourcePos{Line: 28, Column: 1, Byte: 371}, End: SourcePos{Line: 28, Column: 9, Byte: 379}, }, }, { AttributeValue( Error, "simple_attr", "detail", cty.Path{ cty.GetAttrStep{Name: "simple_attr"}, }, ), &SourceRange{ Filename: "test.tf", Start: SourcePos{Line: 32, Column: 15, Byte: 434}, End: SourcePos{Line: 32, Column: 20, Byte: 439}, }, }, { // This should never happen as error should always point to an attribute // or index of an attribute, but we should not crash if it does AttributeValue( Error, "key", "index_step", cty.Path{ cty.IndexStep{Key: cty.StringVal("key")}, }, ), emptySrcRng, }, { // This should never happen as error should always point to an attribute // or index of an attribute, but we should not crash if it does AttributeValue( Error, "key.another", "index_step", cty.Path{ cty.IndexStep{Key: cty.StringVal("key")}, cty.IndexStep{Key: cty.StringVal("another")}, }, ), emptySrcRng, }, } for i, tc := range testCases { t.Run(fmt.Sprintf("%d:%s", i, tc.Diag.Description()), func(t *testing.T) { var diags Diagnostics origAddr := tc.Diag.Description().Address diags = diags.Append(tc.Diag) gotDiags := diags.InConfigBody(f.Body, "test.addr") gotRange := gotDiags[0].Source().Subject gotAddr := gotDiags[0].Description().Address switch { case origAddr != "": if gotAddr != origAddr { t.Errorf("original diagnostic address modified from %s to %s", origAddr, gotAddr) } case gotAddr != "test.addr": t.Error("missing detail address") } for _, problem := range deep.Equal(gotRange, tc.ExpectedRange) { t.Error(problem) } }) } } func TestGetAttribute(t *testing.T) { path := cty.Path{ cty.GetAttrStep{Name: "foo"}, cty.IndexStep{Key: cty.NumberIntVal(0)}, cty.GetAttrStep{Name: "bar"}, } d := AttributeValue( Error, "foo[0].bar", "detail", path, ) p := GetAttribute(d) if !reflect.DeepEqual(path, p) { t.Fatalf("paths don't match:\nexpected: %#v\ngot: %#v", path, p) } }