From 6ff0526809488617aa22f1bf17b449a75ec82cef Mon Sep 17 00:00:00 2001 From: Daniel Banck Date: Tue, 20 Feb 2024 11:07:16 +0100 Subject: [PATCH] Fix `for` expression collection constraints (#375) * Update contraints for `for` collection expr * Add test case for `for` in `for_each` attr * Fix existing `for` tests --- decoder/expr_any_for.go | 10 ++- decoder/expr_any_ref_origins_test.go | 129 ++++++++++++++++++++++++++- decoder/expr_one_of_ref_origins.go | 2 +- 3 files changed, 136 insertions(+), 5 deletions(-) diff --git a/decoder/expr_any_for.go b/decoder/expr_any_for.go index b1cce89d..57943e81 100644 --- a/decoder/expr_any_for.go +++ b/decoder/expr_any_for.go @@ -175,7 +175,15 @@ func (a Any) refOriginsForForExpr(ctx context.Context, allowSelfRefs bool) (refe // TODO: eType.KeyVarExpr.Range() to collect key as origin // TODO: eType.ValVarExpr.Range() to collect value as origin - if collExpr, ok := newExpression(a.pathCtx, eType.CollExpr, a.cons).(ReferenceOriginsExpression); ok { + // A for expression's input can be a list, a set, a tuple, a map, or an object + collCons := schema.OneOf{ + schema.AnyExpression{OfType: cty.List(cty.DynamicPseudoType)}, + schema.AnyExpression{OfType: cty.Set(cty.DynamicPseudoType)}, + schema.AnyExpression{OfType: cty.EmptyTuple}, + schema.AnyExpression{OfType: cty.Map(cty.DynamicPseudoType)}, + schema.AnyExpression{OfType: cty.EmptyObject}, + } + if collExpr, ok := newExpression(a.pathCtx, eType.CollExpr, collCons).(ReferenceOriginsExpression); ok { origins = append(origins, collExpr.ReferenceOrigins(ctx, allowSelfRefs)...) } diff --git a/decoder/expr_any_ref_origins_test.go b/decoder/expr_any_ref_origins_test.go index 5eea5f3d..6dcb031e 100644 --- a/decoder/expr_any_ref_origins_test.go +++ b/decoder/expr_any_ref_origins_test.go @@ -1049,7 +1049,11 @@ func TestCollectRefOrigins_exprAny_forExpr_hcl(t *testing.T) { End: hcl.Pos{Line: 1, Column: 33, Byte: 32}, }, Constraints: reference.OriginConstraints{ - {OfType: cty.List(cty.String)}, + {OfType: cty.List(cty.DynamicPseudoType)}, + {OfType: cty.Set(cty.DynamicPseudoType)}, + {OfType: cty.EmptyTuple}, + {OfType: cty.Map(cty.DynamicPseudoType)}, + {OfType: cty.EmptyObject}, }, }, reference.LocalOrigin{ @@ -1090,7 +1094,11 @@ func TestCollectRefOrigins_exprAny_forExpr_hcl(t *testing.T) { End: hcl.Pos{Line: 1, Column: 33, Byte: 32}, }, Constraints: reference.OriginConstraints{ - {OfType: cty.Set(cty.String)}, + {OfType: cty.List(cty.DynamicPseudoType)}, + {OfType: cty.Set(cty.DynamicPseudoType)}, + {OfType: cty.EmptyTuple}, + {OfType: cty.Map(cty.DynamicPseudoType)}, + {OfType: cty.EmptyObject}, }, }, reference.LocalOrigin{ @@ -1131,7 +1139,11 @@ func TestCollectRefOrigins_exprAny_forExpr_hcl(t *testing.T) { End: hcl.Pos{Line: 1, Column: 33, Byte: 32}, }, Constraints: reference.OriginConstraints{ + {OfType: cty.List(cty.DynamicPseudoType)}, + {OfType: cty.Set(cty.DynamicPseudoType)}, {OfType: cty.EmptyTuple}, + {OfType: cty.Map(cty.DynamicPseudoType)}, + {OfType: cty.EmptyObject}, }, }, reference.LocalOrigin{ @@ -1172,7 +1184,11 @@ func TestCollectRefOrigins_exprAny_forExpr_hcl(t *testing.T) { End: hcl.Pos{Line: 1, Column: 33, Byte: 32}, }, Constraints: reference.OriginConstraints{ - {OfType: cty.Map(cty.String)}, + {OfType: cty.List(cty.DynamicPseudoType)}, + {OfType: cty.Set(cty.DynamicPseudoType)}, + {OfType: cty.EmptyTuple}, + {OfType: cty.Map(cty.DynamicPseudoType)}, + {OfType: cty.EmptyObject}, }, }, reference.LocalOrigin{ @@ -1226,6 +1242,10 @@ func TestCollectRefOrigins_exprAny_forExpr_hcl(t *testing.T) { End: hcl.Pos{Line: 1, Column: 33, Byte: 32}, }, Constraints: reference.OriginConstraints{ + {OfType: cty.List(cty.DynamicPseudoType)}, + {OfType: cty.Set(cty.DynamicPseudoType)}, + {OfType: cty.EmptyTuple}, + {OfType: cty.Map(cty.DynamicPseudoType)}, {OfType: cty.EmptyObject}, }, }, @@ -1288,6 +1308,109 @@ func TestCollectRefOrigins_exprAny_forExpr_hcl(t *testing.T) { } } +func TestCollectRefOrigins_exprAny_forExpr_forEach(t *testing.T) { + bodySchema := &schema.BodySchema{ + Blocks: map[string]*schema.BlockSchema{ + "resource": { + Labels: []*schema.LabelSchema{ + {Name: "type"}, {Name: "name"}, + }, + Body: &schema.BodySchema{ + Extensions: &schema.BodyExtensions{ + ForEach: true, + }, + }, + }, + }, + } + + cfg := `resource "aws_instance" "foo" { + for_each = { for i in var.coll : i.name => i } +}` + + f, diags := hclsyntax.ParseConfig([]byte(cfg), "test.tf", hcl.InitialPos) + if len(diags) > 0 { + t.Error(diags) + } + d := testPathDecoder(t, &PathContext{ + Schema: bodySchema, + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) + + origins, err := d.CollectReferenceOrigins() + if err != nil { + t.Fatal(err) + } + + expectedRefOrigins := reference.Origins{ + reference.LocalOrigin{ + Addr: lang.Address{ + lang.RootStep{Name: "var"}, + lang.AttrStep{Name: "coll"}, + }, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 25, Byte: 56}, + End: hcl.Pos{Line: 2, Column: 33, Byte: 64}, + }, + Constraints: reference.OriginConstraints{ + {OfType: cty.List(cty.DynamicPseudoType)}, + {OfType: cty.Set(cty.DynamicPseudoType)}, + {OfType: cty.EmptyTuple}, + {OfType: cty.Map(cty.DynamicPseudoType)}, + {OfType: cty.EmptyObject}, + {OfType: cty.List(cty.DynamicPseudoType)}, + {OfType: cty.Set(cty.DynamicPseudoType)}, + {OfType: cty.EmptyTuple}, + {OfType: cty.Map(cty.DynamicPseudoType)}, + {OfType: cty.EmptyObject}, + {OfType: cty.List(cty.DynamicPseudoType)}, + {OfType: cty.Set(cty.DynamicPseudoType)}, + {OfType: cty.EmptyTuple}, + {OfType: cty.Map(cty.DynamicPseudoType)}, + {OfType: cty.EmptyObject}, + }, + }, + reference.LocalOrigin{ + Addr: lang.Address{ + lang.RootStep{Name: "i"}, + lang.AttrStep{Name: "name"}, + }, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 36, Byte: 67}, + End: hcl.Pos{Line: 2, Column: 42, Byte: 73}, + }, + Constraints: reference.OriginConstraints{ + {OfType: cty.String}, + {OfType: cty.String}, + {OfType: cty.String}, + }, + }, + reference.LocalOrigin{ + Addr: lang.Address{ + lang.RootStep{Name: "i"}, + }, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 46, Byte: 77}, + End: hcl.Pos{Line: 2, Column: 47, Byte: 78}, + }, + Constraints: reference.OriginConstraints{ + {OfType: cty.DynamicPseudoType}, + {OfType: cty.String}, + {OfType: cty.DynamicPseudoType}, + }, + }, + } + + if diff := cmp.Diff(expectedRefOrigins, origins, ctydebug.CmpOptions); diff != "" { + t.Fatalf("unexpected origins: %s", diff) + } +} + func TestCollectRefOrigins_exprAny_operators_json(t *testing.T) { testCases := []struct { testName string diff --git a/decoder/expr_one_of_ref_origins.go b/decoder/expr_one_of_ref_origins.go index c8aec289..49a3d0a7 100644 --- a/decoder/expr_one_of_ref_origins.go +++ b/decoder/expr_one_of_ref_origins.go @@ -44,7 +44,7 @@ func appendOrigins(origins, newOrigins reference.Origins) reference.Origins { if ok && existingOrigin.Address().Equals(newMatchableOrigin.Address()) && rangesEqual(existingOrigin.OriginRange(), newMatchableOrigin.OriginRange()) { - + // TODO? deduplicate constraints origins[i] = existingOrigin.AppendConstraints(newMatchableOrigin.OriginConstraints()) foundMatch = true break