fix(genconfig): properly quote attribute names when necessary

Ensure attribute names are correctly quoted in generated config when they contain
characters that make them invalid identifiers (e.g., dots, double quotes).

- Use `hclsyntax.ValidIdentifier` to check if an attribute name needs quoting.
- Convert invalid identifiers to valid HCL strings using `hclwrite.TokensForValue`.
- Add test cases for attributes with dots and double quotes to verify correctness.

Signed-off-by: James Humphries <james@james-humphries.co.uk>
This commit is contained in:
James Humphries 2025-02-19 15:50:58 +00:00
parent de95b65faa
commit 7af38480a2
No known key found for this signature in database
GPG Key ID: 6463EF67AEEADE49
2 changed files with 111 additions and 4 deletions

View File

@ -12,6 +12,7 @@ import (
"strings"
"github.com/hashicorp/hcl/v2"
hclsyntax "github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/hashicorp/hcl/v2/hclwrite"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/json"
@ -90,6 +91,10 @@ func writeConfigAttributes(addr addrs.AbsResourceInstance, buf *strings.Builder,
}
if attrS.Required {
buf.WriteString(strings.Repeat(" ", indent))
// Handle cases where the name should be contained in quotes
if !hclsyntax.ValidIdentifier(name) {
name = string(hclwrite.TokensForValue(cty.StringVal(name)).Bytes())
}
buf.WriteString(fmt.Sprintf("%s = ", name))
tok := hclwrite.TokensForValue(attrS.EmptyValue())
if _, err := tok.WriteTo(buf); err != nil {
@ -104,6 +109,10 @@ func writeConfigAttributes(addr addrs.AbsResourceInstance, buf *strings.Builder,
writeAttrTypeConstraint(buf, attrS)
} else if attrS.Optional {
buf.WriteString(strings.Repeat(" ", indent))
// Handle cases where the name should be contained in quotes
if !hclsyntax.ValidIdentifier(name) {
name = string(hclwrite.TokensForValue(cty.StringVal(name)).Bytes())
}
buf.WriteString(fmt.Sprintf("%s = ", name))
tok := hclwrite.TokensForValue(attrS.EmptyValue())
if _, err := tok.WriteTo(buf); err != nil {

View File

@ -82,6 +82,104 @@ resource "tfcoremock_simple_resource" "empty" {
list_block { # OPTIONAL block
nested_value = null # OPTIONAL string
}
}`,
},
"simple_resource_with_propertyname_containing_a_dot": {
schema: &configschema.Block{
BlockTypes: map[string]*configschema.NestedBlock{
"list_block": {
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"nested_value.json": {
Type: cty.String,
Optional: true,
},
},
},
Nesting: configschema.NestingSingle,
},
},
Attributes: map[string]*configschema.Attribute{
"id": {
Type: cty.String,
Computed: true,
},
"value": {
Type: cty.String,
Optional: true,
},
},
},
addr: addrs.AbsResourceInstance{
Module: nil,
Resource: addrs.ResourceInstance{
Resource: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "tfcoremock_simple_resource",
Name: "empty",
},
Key: nil,
},
},
provider: addrs.LocalProviderConfig{
LocalName: "tfcoremock",
},
value: cty.NilVal,
expected: `
resource "tfcoremock_simple_resource" "empty" {
value = null # OPTIONAL string
list_block { # OPTIONAL block
"nested_value.json" = null # OPTIONAL string
}
}`,
},
"simple_resource_with_propertyname_containing_double_quotes": {
schema: &configschema.Block{
BlockTypes: map[string]*configschema.NestedBlock{
"list_block": {
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"nested_value\"example": {
Type: cty.String,
Optional: true,
},
},
},
Nesting: configschema.NestingSingle,
},
},
Attributes: map[string]*configschema.Attribute{
"id": {
Type: cty.String,
Computed: true,
},
"value": {
Type: cty.String,
Optional: true,
},
},
},
addr: addrs.AbsResourceInstance{
Module: nil,
Resource: addrs.ResourceInstance{
Resource: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "tfcoremock_simple_resource",
Name: "empty",
},
Key: nil,
},
},
provider: addrs.LocalProviderConfig{
LocalName: "tfcoremock",
},
value: cty.NilVal,
expected: `
resource "tfcoremock_simple_resource" "empty" {
value = null # OPTIONAL string
list_block { # OPTIONAL block
"nested_value\"example" = null # OPTIONAL string
}
}`,
},
"simple_resource_with_state": {
@ -670,20 +768,20 @@ resource "tfcoremock_simple_resource" "example" {
},
}
for name, tc := range tcs {
t.Run(name, func(t *testing.T) {
t.Run(name, func(tt *testing.T) {
err := tc.schema.InternalValidate()
if err != nil {
t.Fatalf("schema failed InternalValidate: %s", err)
tt.Fatalf("schema failed InternalValidate: %s", err)
}
contents, diags := GenerateResourceContents(tc.addr, tc.schema, tc.provider, tc.value)
if len(diags) > 0 {
t.Errorf("expected no diagnostics but found %s", diags)
tt.Errorf("expected no diagnostics but found %s", diags)
}
got := WrapResourceContents(tc.addr, contents)
want := strings.TrimSpace(tc.expected)
if diff := cmp.Diff(got, want); len(diff) > 0 {
t.Errorf("got:\n%s\nwant:\n%s\ndiff:\n%s", got, want, diff)
tt.Errorf("got:\n%s\nwant:\n%s\ndiff:\n%s", got, want, diff)
}
})
}