Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Detect when provider and resource/module have identical for_each #2186

Merged
merged 12 commits into from
Dec 3, 2024
Merged
100 changes: 100 additions & 0 deletions internal/addrs/traversal.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package addrs

import (
"bytes"
"fmt"

"github.com/hashicorp/hcl/v2"
"github.com/zclconf/go-cty/cty"
)

// TraversalStr produces a representation of an HCL traversal that is compact,
// resembles HCL native syntax, and is suitable for display in the UI.
//
// This function is primarily to help with including traversal strings in
// the UI, and in particular should not be used for comparing traversals.
// Use [TraversalsEquivalent] to determine whether two traversals have
// the same meaning.
func TraversalStr(traversal hcl.Traversal) string {
cam72cam marked this conversation as resolved.
Show resolved Hide resolved
var buf bytes.Buffer
for _, step := range traversal {
switch tStep := step.(type) {
case hcl.TraverseRoot:
buf.WriteString(tStep.Name)
case hcl.TraverseAttr:
buf.WriteByte('.')
buf.WriteString(tStep.Name)
case hcl.TraverseIndex:
buf.WriteByte('[')
switch tStep.Key.Type() {
case cty.String:
buf.WriteString(fmt.Sprintf("%q", tStep.Key.AsString()))
case cty.Number:
bf := tStep.Key.AsBigFloat()
//nolint:mnd // numerical precision
buf.WriteString(bf.Text('g', 10))
default:
buf.WriteString("...")
}
buf.WriteByte(']')
}
}
return buf.String()
}

// TraversalsEquivalent returns true if the two given traversals represent
// the same meaning to HCL in all contexts.
//
// Unfortunately there is some ambiguity in interpreting traversal equivalence
// because HCL treats them differently depending on the context. If a
// traversal is involved in expression evaluation then the [hcl.Index]
// and [hcl.GetAttr] functions perform some automatic type conversions and
// allow interchanging index vs. attribute syntax for map and object types,
// but when interpreting traversals just for their syntax (as we do in
// [ParseRef], for example) these distinctions can potentially be significant.
//
// This function takes the stricter interpretation of ignoring the automatic
// adaptations made during expression evaluation, and so for example
// foo.bar and foo["bar"] are NOT considered to be equivalent by this function.
func TraversalsEquivalent(a, b hcl.Traversal) bool {
if len(a) != len(b) {
return false
}

for idx, stepA := range a {
stepB := b[idx]

switch stepA := stepA.(type) {
case hcl.TraverseRoot:
stepB, ok := stepB.(hcl.TraverseRoot)
if !ok || stepA.Name != stepB.Name {
return false
}
case hcl.TraverseAttr:
stepB, ok := stepB.(hcl.TraverseAttr)
if !ok || stepA.Name != stepB.Name {
return false
}
case hcl.TraverseIndex:
stepB, ok := stepB.(hcl.TraverseIndex)
if !ok || stepA.Key.Equals(stepB.Key) != cty.True {
return false
}
default:
// The above should be exhaustive for all traversal
// step types that HCL can possibly generate. We'll
// treat any unsupported stepA types as non-equal
// because that matches what would happen if
// any stepB were unsupported: the type assertions
// in the above cases would fail.
return false
}
}

return true
}
169 changes: 169 additions & 0 deletions internal/addrs/traversal_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package addrs

import (
"fmt"
"testing"

"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
)

func TestTraversalsEquivalent(t *testing.T) {
tests := []struct {
A, B string
Equivalent bool
}{
{
`foo`,
`foo`,
true,
},
{
`foo`,
`foo.bar`,
false,
},
{
`foo.bar`,
`foo`,
false,
},
{
`foo`,
`bar`,
false,
},
{
`foo.bar`,
`foo.bar`,
true,
},
{
`foo.bar`,
`foo.baz`,
false,
},
{
`foo["bar"]`,
`foo["bar"]`,
true,
},
{
`foo["bar"]`,
`foo["baz"]`,
false,
},
{
`foo[0]`,
`foo[0]`,
true,
},
{
`foo[0]`,
`foo[1]`,
false,
},
{
`foo[0]`,
`foo["0"]`,
false,
},
{
`foo["0"]`,
`foo[0]`,
false,
},
{
// HCL considers these distinct syntactically but considers them
// equivalent during expression evaluation, so whether to consider
// these equivalent is unfortunately context-dependent. We take
// the more conservative interpretation of considering them to
// be distinct.
`foo["bar"]`,
`foo.bar`,
false,
},
{
// The following strings differ only in the level of unicode
// normalization. HCL considers two strings to be equal if they
// have identical unicode normalization.
`foo["ba\u0301z"]`,
`foo["b\u00e1z"]`,
true,
},
{
`foo[1.0]`,
`foo[1]`,
true,
},
{
`foo[01]`,
`foo[1]`,
true,
},
{
// A traversal with a non-integral numeric index is strange, but
// is permitted by HCL syntactically. It would be rejected during
// expression evaluation.
`foo[1.2]`,
`foo[1]`,
false,
},
{
// A traversal with a non-integral numeric index is strange, but
// is permitted by HCL syntactically. It would be rejected during
// expression evaluation.
`foo[1.2]`,
`foo[1.2]`,
true,
},
{
// Integers too large to fit into the significand of a float64
// historically caused some grief for HCL and cty, but this should
// be fixed now and so the following should compare as different.
`foo[9223372036854775807]`,
`foo[9223372036854775808]`,
false,
},
{
// As above, but these two _equal_ large integers should compare
// as equivalent.
`foo[9223372036854775807]`,
`foo[9223372036854775807]`,
true,
},
{
`foo[3.14159265358979323846264338327950288419716939937510582097494459]`,
`foo[3.14159265358979323846264338327950288419716939937510582097494459]`,
true,
},
// HCL and cty also have some numeric comparison quirks with floats
// that lack an exact base-2 representation and zero vs. negative zero,
// but those quirks can't arise from parsing a traversal -- only from
// dynamic expression evaluation -- so we don't need to (and cannot)
// check them here.
}

for _, test := range tests {
t.Run(fmt.Sprintf("%s ≡ %s", test.A, test.B), func(t *testing.T) {
a, diags := hclsyntax.ParseTraversalAbs([]byte(test.A), "", hcl.InitialPos)
if diags.HasErrors() {
t.Fatalf("input A has invalid syntax: %s", diags.Error())
}
b, diags := hclsyntax.ParseTraversalAbs([]byte(test.B), "", hcl.InitialPos)
if diags.HasErrors() {
t.Fatalf("input B has invalid syntax: %s", diags.Error())
}

got := TraversalsEquivalent(a, b)
if want := test.Equivalent; got != want {
t.Errorf("wrong result\ninput A: %s\ninput B: %s\ngot: %t\nwant: %t", test.A, test.B, got, want)
}
})
}
}
31 changes: 1 addition & 30 deletions internal/command/jsonconfig/expression.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
package jsonconfig

import (
"bytes"
"encoding/json"
"fmt"

Expand Down Expand Up @@ -59,7 +58,7 @@ func marshalExpression(ex hcl.Expression) expression {
// into parts until we end up at the smallest referenceable address.
remains := ref.Remaining
for len(remains) > 0 {
varString = append(varString, fmt.Sprintf("%s%s", ref.Subject, traversalStr(remains)))
varString = append(varString, fmt.Sprintf("%s%s", ref.Subject, addrs.TraversalStr(remains)))
remains = remains[:(len(remains) - 1)]
}
varString = append(varString, ref.Subject.String())
Expand Down Expand Up @@ -157,31 +156,3 @@ func marshalExpressions(body hcl.Body, schema *configschema.Block) expressions {

return ret
}

// traversalStr produces a representation of an HCL traversal that is compact,
// resembles HCL native syntax, and is suitable for display in the UI.
//
// This was copied (and simplified) from internal/command/views/json/diagnostic.go.
func traversalStr(traversal hcl.Traversal) string {
var buf bytes.Buffer
for _, step := range traversal {
switch tStep := step.(type) {
case hcl.TraverseRoot:
buf.WriteString(tStep.Name)
case hcl.TraverseAttr:
buf.WriteByte('.')
buf.WriteString(tStep.Name)
case hcl.TraverseIndex:
buf.WriteByte('[')
switch tStep.Key.Type() {
case cty.String:
buf.WriteString(fmt.Sprintf("%q", tStep.Key.AsString()))
case cty.Number:
bf := tStep.Key.AsBigFloat()
buf.WriteString(bf.Text('g', 10))
}
buf.WriteByte(']')
}
}
return buf.String()
}
Loading
Loading