diff --git a/decoder/expr_list_ref_targets.go b/decoder/expr_list_ref_targets.go index a6db3dff..9526ffdd 100644 --- a/decoder/expr_list_ref_targets.go +++ b/decoder/expr_list_ref_targets.go @@ -5,28 +5,33 @@ import ( "github.com/hashicorp/hcl-lang/lang" "github.com/hashicorp/hcl-lang/reference" - "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/hashicorp/hcl-lang/schema" + "github.com/hashicorp/hcl/v2" "github.com/zclconf/go-cty/cty" ) func (list List) ReferenceTargets(ctx context.Context, targetCtx *TargetContext) reference.Targets { - eType, ok := list.expr.(*hclsyntax.TupleConsExpr) - if !ok { + elems, diags := hcl.ExprList(list.expr) + if diags.HasErrors() { return reference.Targets{} } - if len(eType.Exprs) == 0 || list.cons.Elem == nil { + if list.cons.Elem == nil { return reference.Targets{} } - targets := make(reference.Targets, 0) - - // TODO: collect parent target for the whole list - // See https://github.com/hashicorp/hcl-lang/issues/228 + elemTargets := make(reference.Targets, 0) - for i, elemExpr := range eType.Exprs { + for i, elemExpr := range elems { expr := newExpression(list.pathCtx, elemExpr, list.cons.Elem) if e, ok := expr.(ReferenceTargetsExpression); ok { + if targetCtx == nil { + // collect any targets inside the expression + // if attribute itself isn't targetable + elemTargets = append(elemTargets, e.ReferenceTargets(ctx, nil)...) + continue + } + elemCtx := targetCtx.Copy() elemCtx.ParentAddress = append(elemCtx.ParentAddress, lang.IndexStep{ Key: cty.NumberIntVal(int64(i)), @@ -37,8 +42,58 @@ func (list List) ReferenceTargets(ctx context.Context, targetCtx *TargetContext) }) } - targets = append(targets, e.ReferenceTargets(ctx, elemCtx)...) + elemTargets = append(elemTargets, e.ReferenceTargets(ctx, elemCtx)...) + } + } + + targets := make(reference.Targets, 0) + + if targetCtx != nil { + // collect target for the whole list + + var rangePtr *hcl.Range + if targetCtx.ParentRangePtr != nil { + rangePtr = targetCtx.ParentRangePtr + } else { + rangePtr = list.expr.Range().Ptr() + } + + // type-aware + elemCons, ok := list.cons.Elem.(schema.TypeAwareConstraint) + if targetCtx.AsExprType && ok { + elemType, ok := elemCons.ConstraintType() + if ok { + targets = append(targets, reference.Target{ + Addr: targetCtx.ParentAddress, + Name: targetCtx.FriendlyName, + Type: cty.List(elemType), + ScopeId: targetCtx.ScopeId, + RangePtr: rangePtr, + DefRangePtr: targetCtx.ParentDefRangePtr, + NestedTargets: elemTargets, + LocalAddr: targetCtx.ParentLocalAddress, + TargetableFromRangePtr: targetCtx.TargetableFromRangePtr, + }) + } + } + + // type-unaware + if targetCtx.AsReference { + targets = append(targets, reference.Target{ + Addr: targetCtx.ParentAddress, + Name: targetCtx.FriendlyName, + ScopeId: targetCtx.ScopeId, + RangePtr: rangePtr, + DefRangePtr: targetCtx.ParentDefRangePtr, + NestedTargets: elemTargets, + LocalAddr: targetCtx.ParentLocalAddress, + TargetableFromRangePtr: targetCtx.TargetableFromRangePtr, + }) } + } else { + // treat element targets as 1st class ones + // if the list itself isn't targetable + targets = elemTargets } return targets diff --git a/decoder/expr_list_ref_targets_test.go b/decoder/expr_list_ref_targets_test.go new file mode 100644 index 00000000..88773159 --- /dev/null +++ b/decoder/expr_list_ref_targets_test.go @@ -0,0 +1,764 @@ +package decoder + +import ( + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/hcl-lang/lang" + "github.com/hashicorp/hcl-lang/reference" + "github.com/hashicorp/hcl-lang/schema" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/hashicorp/hcl/v2/json" + "github.com/zclconf/go-cty-debug/ctydebug" + "github.com/zclconf/go-cty/cty" +) + +func TestCollectRefTargets_exprList_hcl(t *testing.T) { + testCases := []struct { + testName string + attrSchema map[string]*schema.AttributeSchema + cfg string + expectedRefTargets reference.Targets + }{ + { + "constraint mismatch", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.List{ + Elem: schema.LiteralType{Type: cty.Bool}, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `attr = true`, + reference.Targets{}, + }, + { + "list of keyword", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.List{ + Elem: schema.Keyword{Keyword: "foo"}, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `attr = [foo]`, + reference.Targets{}, + }, + { + "list of addressable reference", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.List{ + Elem: schema.Reference{ + Address: &schema.ReferenceAddrSchema{ + ScopeId: lang.ScopeId("test"), + }, + }, + }, + IsOptional: true, + }, + }, + `attr = [foo]`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "foo"}, + }, + ScopeId: lang.ScopeId("test"), + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 9, Byte: 8}, + End: hcl.Pos{Line: 1, Column: 12, Byte: 11}, + }, + }, + }, + }, + { + "empty type-aware", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.List{ + Elem: schema.LiteralType{ + Type: cty.String, + }, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `attr = []`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + Type: cty.List(cty.String), + NestedTargets: reference.Targets{}, + }, + }, + }, + { + "type-aware with invalid element", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.List{ + Elem: schema.LiteralType{ + Type: cty.String, + }, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `attr = ["one", foo, "two"]`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 27, Byte: 26}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + Type: cty.List(cty.String), + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(0)}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 9, Byte: 8}, + End: hcl.Pos{Line: 1, Column: 14, Byte: 13}, + }, + Type: cty.String, + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(2)}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 21, Byte: 20}, + End: hcl.Pos{Line: 1, Column: 26, Byte: 25}, + }, + Type: cty.String, + }, + }, + }, + }, + }, + { + "type-unaware", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.List{ + Elem: schema.LiteralType{ + Type: cty.String, + }, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + ScopeId: lang.ScopeId("test"), + AsReference: true, + }, + }, + }, + `attr = ["one", "two"]`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 22, Byte: 21}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + ScopeId: lang.ScopeId("test"), + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(0)}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 9, Byte: 8}, + End: hcl.Pos{Line: 1, Column: 14, Byte: 13}, + }, + ScopeId: lang.ScopeId("test"), + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(1)}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 16, Byte: 15}, + End: hcl.Pos{Line: 1, Column: 21, Byte: 20}, + }, + ScopeId: lang.ScopeId("test"), + }, + }, + }, + }, + }, + { + "type-aware nested", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.List{ + Elem: schema.List{ + Elem: schema.LiteralType{ + Type: cty.String, + }, + }, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `attr = [ + ["one"], + ["two"], +] +`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 4, Column: 2, Byte: 32}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + Type: cty.List(cty.List(cty.String)), + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(0)}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 11}, + End: hcl.Pos{Line: 2, Column: 10, Byte: 18}, + }, + Type: cty.List(cty.String), + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(0)}, + lang.IndexStep{Key: cty.NumberIntVal(0)}, + }, + Type: cty.String, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 2, Column: 4, Byte: 12}, + End: hcl.Pos{Line: 2, Column: 9, Byte: 17}, + }, + }, + }, + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(1)}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 22}, + End: hcl.Pos{Line: 3, Column: 10, Byte: 29}, + }, + Type: cty.List(cty.String), + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(1)}, + lang.IndexStep{Key: cty.NumberIntVal(0)}, + }, + Type: cty.String, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 3, Column: 4, Byte: 23}, + End: hcl.Pos{Line: 3, Column: 9, Byte: 28}, + }, + }, + }, + }, + }, + }, + }, + }, + } + for i, tc := range testCases { + t.Run(fmt.Sprintf("%d-%s", i, tc.testName), func(t *testing.T) { + bodySchema := &schema.BodySchema{ + Attributes: tc.attrSchema, + } + + f, diags := hclsyntax.ParseConfig([]byte(tc.cfg), "test.hcl", hcl.InitialPos) + if len(diags) > 0 { + t.Error(diags) + } + d := testPathDecoder(t, &PathContext{ + Schema: bodySchema, + Files: map[string]*hcl.File{ + "test.hcl": f, + }, + }) + + targets, err := d.CollectReferenceTargets() + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(tc.expectedRefTargets, targets, ctydebug.CmpOptions); diff != "" { + t.Fatalf("unexpected targets: %s", diff) + } + }) + } +} + +func TestCollectRefTargets_exprList_json(t *testing.T) { + testCases := []struct { + testName string + attrSchema map[string]*schema.AttributeSchema + cfg string + expectedRefTargets reference.Targets + }{ + { + "constraint mismatch", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.List{ + Elem: schema.LiteralType{Type: cty.Bool}, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `{"attr": true}`, + reference.Targets{}, + }, + { + "list of keyword", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.List{ + Elem: schema.Keyword{Keyword: "foo"}, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `{"attr": ["foo"]}`, + reference.Targets{}, + }, + { + "list of addressable reference", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.List{ + Elem: schema.Reference{ + Address: &schema.ReferenceAddrSchema{ + ScopeId: lang.ScopeId("test"), + }, + }, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + }, + }, + }, + `{"attr": ["foo"]}`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "foo"}, + }, + ScopeId: lang.ScopeId("test"), + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 12, Byte: 11}, + End: hcl.Pos{Line: 1, Column: 15, Byte: 14}, + }, + }, + }, + }, + { + "empty type-aware", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.List{ + Elem: schema.LiteralType{ + Type: cty.String, + }, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `{"attr": []}`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 12, Byte: 11}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + Type: cty.List(cty.String), + NestedTargets: reference.Targets{}, + }, + }, + }, + { + "type-aware with invalid element", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.List{ + Elem: schema.LiteralType{ + Type: cty.String, + }, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `{"attr": ["one", 422, "two"]}`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 29, Byte: 28}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + Type: cty.List(cty.String), + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(0)}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 11, Byte: 10}, + End: hcl.Pos{Line: 1, Column: 16, Byte: 15}, + }, + Type: cty.String, + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(2)}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 23, Byte: 22}, + End: hcl.Pos{Line: 1, Column: 28, Byte: 27}, + }, + Type: cty.String, + }, + }, + }, + }, + }, + { + "type-unaware", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.List{ + Elem: schema.LiteralType{ + Type: cty.String, + }, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + ScopeId: lang.ScopeId("test"), + AsReference: true, + }, + }, + }, + `{"attr": ["one", "two"]}`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 24, Byte: 23}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + ScopeId: lang.ScopeId("test"), + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(0)}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 11, Byte: 10}, + End: hcl.Pos{Line: 1, Column: 16, Byte: 15}, + }, + ScopeId: lang.ScopeId("test"), + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(1)}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 18, Byte: 17}, + End: hcl.Pos{Line: 1, Column: 23, Byte: 22}, + }, + ScopeId: lang.ScopeId("test"), + }, + }, + }, + }, + }, + { + "type-aware nested", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.List{ + Elem: schema.List{ + Elem: schema.LiteralType{ + Type: cty.String, + }, + }, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `{"attr": [ + ["one"], + ["two"] +]}`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 4, Column: 2, Byte: 33}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + Type: cty.List(cty.List(cty.String)), + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(0)}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 13}, + End: hcl.Pos{Line: 2, Column: 10, Byte: 20}, + }, + Type: cty.List(cty.String), + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(0)}, + lang.IndexStep{Key: cty.NumberIntVal(0)}, + }, + Type: cty.String, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 2, Column: 4, Byte: 14}, + End: hcl.Pos{Line: 2, Column: 9, Byte: 19}, + }, + }, + }, + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(1)}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 24}, + End: hcl.Pos{Line: 3, Column: 10, Byte: 31}, + }, + Type: cty.List(cty.String), + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(1)}, + lang.IndexStep{Key: cty.NumberIntVal(0)}, + }, + Type: cty.String, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 3, Column: 4, Byte: 25}, + End: hcl.Pos{Line: 3, Column: 9, Byte: 30}, + }, + }, + }, + }, + }, + }, + }, + }, + } + for i, tc := range testCases { + t.Run(fmt.Sprintf("%d-%s", i, tc.testName), func(t *testing.T) { + bodySchema := &schema.BodySchema{ + Attributes: tc.attrSchema, + } + + f, diags := json.ParseWithStartPos([]byte(tc.cfg), "test.hcl.json", hcl.InitialPos) + if len(diags) > 0 { + t.Error(diags) + } + d := testPathDecoder(t, &PathContext{ + Schema: bodySchema, + Files: map[string]*hcl.File{ + "test.hcl.json": f, + }, + }) + + targets, err := d.CollectReferenceTargets() + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(tc.expectedRefTargets, targets, ctydebug.CmpOptions); diff != "" { + t.Fatalf("unexpected targets: %s", diff) + } + }) + } +} diff --git a/decoder/expr_literal_type.go b/decoder/expr_literal_type.go index 57279383..c1249187 100644 --- a/decoder/expr_literal_type.go +++ b/decoder/expr_literal_type.go @@ -3,6 +3,7 @@ package decoder import ( "github.com/hashicorp/hcl-lang/schema" "github.com/hashicorp/hcl/v2" + "github.com/zclconf/go-cty/cty" ) type LiteralType struct { @@ -11,3 +12,19 @@ type LiteralType struct { pathCtx *PathContext } + +func (lt LiteralType) InferType() (cty.Type, bool) { + consType, ok := lt.cons.ConstraintType() + if !ok { + return consType, false + } + + if consType == cty.DynamicPseudoType && !isEmptyExpression(lt.expr) { + val, diags := lt.expr.Value(nil) + if !diags.HasErrors() { + consType = val.Type() + } + } + + return consType, true +} diff --git a/decoder/expr_literal_type_ref_targets.go b/decoder/expr_literal_type_ref_targets.go index 6934a593..abad8b0a 100644 --- a/decoder/expr_literal_type_ref_targets.go +++ b/decoder/expr_literal_type_ref_targets.go @@ -5,59 +5,89 @@ import ( "github.com/hashicorp/hcl-lang/reference" "github.com/hashicorp/hcl-lang/schema" - "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/hashicorp/hcl/v2" + "github.com/zclconf/go-cty/cty" ) func (lt LiteralType) ReferenceTargets(ctx context.Context, targetCtx *TargetContext) reference.Targets { typ := lt.cons.Type - // Primitive types are collected separately on attribute level - if typ.IsPrimitiveType() { + if typ == cty.DynamicPseudoType { + val, diags := lt.expr.Value(&hcl.EvalContext{}) + if !diags.HasErrors() { + typ = val.Type() + } + } + + if targetCtx == nil || len(targetCtx.ParentAddress) == 0 { return reference.Targets{} } - if typ.IsListType() { - expr, ok := lt.expr.(*hclsyntax.TupleConsExpr) - if !ok { - return nil + if typ.IsPrimitiveType() { + if !isEmptyExpression(lt.expr) { + // checking the expression strictly against constraint + // allows us to pick the right one if it's inside OneOf + val, diags := lt.expr.Value(&hcl.EvalContext{}) + if diags.HasErrors() { + return reference.Targets{} + } + if !val.Type().Equals(typ) { + return reference.Targets{} + } + } + + var rangePtr *hcl.Range + if targetCtx.ParentRangePtr != nil { + rangePtr = targetCtx.ParentRangePtr + } else { + rangePtr = lt.expr.Range().Ptr() } + var refType cty.Type + if targetCtx.AsExprType { + refType = typ + } + + return reference.Targets{ + { + Addr: targetCtx.ParentAddress, + LocalAddr: targetCtx.ParentLocalAddress, + TargetableFromRangePtr: targetCtx.TargetableFromRangePtr, + ScopeId: targetCtx.ScopeId, + RangePtr: rangePtr, + DefRangePtr: targetCtx.ParentDefRangePtr, + Type: refType, + }, + } + } + + if typ.IsListType() { list := List{ cons: schema.List{ Elem: schema.LiteralType{ Type: typ.ElementType(), }, }, - expr: expr, + expr: lt.expr, pathCtx: lt.pathCtx, } return list.ReferenceTargets(ctx, targetCtx) } if typ.IsSetType() { - expr, ok := lt.expr.(*hclsyntax.TupleConsExpr) - if !ok { - return nil - } - set := Set{ cons: schema.Set{ Elem: schema.LiteralType{ Type: typ.ElementType(), }, }, - expr: expr, + expr: lt.expr, pathCtx: lt.pathCtx, } return set.ReferenceTargets(ctx, targetCtx) } if typ.IsTupleType() { - expr, ok := lt.expr.(*hclsyntax.TupleConsExpr) - if !ok { - return nil - } - elemTypes := typ.TupleElementTypes() cons := schema.Tuple{ Elems: make([]schema.Constraint, len(elemTypes)), @@ -69,7 +99,7 @@ func (lt LiteralType) ReferenceTargets(ctx context.Context, targetCtx *TargetCon } tuple := Tuple{ cons: cons, - expr: expr, + expr: lt.expr, pathCtx: lt.pathCtx, } @@ -77,34 +107,24 @@ func (lt LiteralType) ReferenceTargets(ctx context.Context, targetCtx *TargetCon } if typ.IsMapType() { - expr, ok := lt.expr.(*hclsyntax.ObjectConsExpr) - if !ok { - return nil - } - m := Map{ cons: schema.Map{ Elem: schema.LiteralType{ Type: typ.ElementType(), }, }, - expr: expr, + expr: lt.expr, pathCtx: lt.pathCtx, } return m.ReferenceTargets(ctx, targetCtx) } if typ.IsObjectType() { - expr, ok := lt.expr.(*hclsyntax.ObjectConsExpr) - if !ok { - return nil - } - obj := Object{ cons: schema.Object{ Attributes: ctyObjectToObjectAttributes(typ), }, - expr: expr, + expr: lt.expr, pathCtx: lt.pathCtx, } return obj.ReferenceTargets(ctx, targetCtx) diff --git a/decoder/expr_literal_type_ref_targets_test.go b/decoder/expr_literal_type_ref_targets_test.go new file mode 100644 index 00000000..40ec90b0 --- /dev/null +++ b/decoder/expr_literal_type_ref_targets_test.go @@ -0,0 +1,3708 @@ +package decoder + +import ( + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/hcl-lang/lang" + "github.com/hashicorp/hcl-lang/reference" + "github.com/hashicorp/hcl-lang/schema" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/hashicorp/hcl/v2/json" + "github.com/zclconf/go-cty-debug/ctydebug" + "github.com/zclconf/go-cty/cty" +) + +func TestCollectRefTargets_exprLiteralType_hcl(t *testing.T) { + testCases := []struct { + testName string + attrSchema map[string]*schema.AttributeSchema + cfg string + expectedRefTargets reference.Targets + }{ + { + "constraint mismatch", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{Type: cty.String}, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `attr = keyword`, + reference.Targets{}, + }, + { + "bool", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{Type: cty.Bool}, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `attr = true`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + Type: cty.Bool, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 12, Byte: 11}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + }, + }, + }, + { + "string", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{Type: cty.String}, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `attr = "foobar"`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + Type: cty.String, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 16, Byte: 15}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + }, + }, + }, + { + "number", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{Type: cty.Number}, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `attr = 42`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + Type: cty.Number, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + }, + }, + }, + { + "list constraint mismatch", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.List(cty.Bool), + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `attr = true`, + reference.Targets{}, + }, + { + "list empty type-aware", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.List(cty.String), + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `attr = []`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + Type: cty.List(cty.String), + NestedTargets: reference.Targets{}, + }, + }, + }, + { + "list type-aware with invalid element", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.List(cty.String), + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `attr = ["one", foo, "two"]`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 27, Byte: 26}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + Type: cty.List(cty.String), + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(0)}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 9, Byte: 8}, + End: hcl.Pos{Line: 1, Column: 14, Byte: 13}, + }, + Type: cty.String, + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(2)}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 21, Byte: 20}, + End: hcl.Pos{Line: 1, Column: 26, Byte: 25}, + }, + Type: cty.String, + }, + }, + }, + }, + }, + { + "list type-unaware", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.List(cty.String), + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + ScopeId: lang.ScopeId("test"), + AsReference: true, + }, + }, + }, + `attr = ["one", "two"]`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 22, Byte: 21}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + ScopeId: lang.ScopeId("test"), + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(0)}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 9, Byte: 8}, + End: hcl.Pos{Line: 1, Column: 14, Byte: 13}, + }, + ScopeId: lang.ScopeId("test"), + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(1)}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 16, Byte: 15}, + End: hcl.Pos{Line: 1, Column: 21, Byte: 20}, + }, + ScopeId: lang.ScopeId("test"), + }, + }, + }, + }, + }, + { + "list type-aware nested", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.List(cty.List(cty.String)), + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `attr = [ + ["one"], + ["two"], +] +`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 4, Column: 2, Byte: 32}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + Type: cty.List(cty.List(cty.String)), + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(0)}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 11}, + End: hcl.Pos{Line: 2, Column: 10, Byte: 18}, + }, + Type: cty.List(cty.String), + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(0)}, + lang.IndexStep{Key: cty.NumberIntVal(0)}, + }, + Type: cty.String, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 2, Column: 4, Byte: 12}, + End: hcl.Pos{Line: 2, Column: 9, Byte: 17}, + }, + }, + }, + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(1)}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 22}, + End: hcl.Pos{Line: 3, Column: 10, Byte: 29}, + }, + Type: cty.List(cty.String), + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(1)}, + lang.IndexStep{Key: cty.NumberIntVal(0)}, + }, + Type: cty.String, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 3, Column: 4, Byte: 23}, + End: hcl.Pos{Line: 3, Column: 9, Byte: 28}, + }, + }, + }, + }, + }, + }, + }, + }, + { + "set constraint mismatch", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.Set(cty.Bool), + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `attr = true`, + reference.Targets{}, + }, + { + "set empty type-aware", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.Set(cty.String), + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `attr = []`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + Type: cty.Set(cty.String), + NestedTargets: reference.Targets{}, + }, + }, + }, + { + "set type-aware with invalid element", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.Set(cty.String), + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `attr = ["one", foo, "two"]`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 27, Byte: 26}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + Type: cty.Set(cty.String), + NestedTargets: reference.Targets{}, + }, + }, + }, + { + "set type-unaware", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.Set(cty.String), + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + ScopeId: lang.ScopeId("test"), + AsReference: true, + }, + }, + }, + `attr = ["one", "two"]`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 22, Byte: 21}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + ScopeId: lang.ScopeId("test"), + NestedTargets: reference.Targets{}, + }, + }, + }, + { + "set type-aware nested", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.Set(cty.Set(cty.String)), + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `attr = [ + ["one"], + ["two"], +] +`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 4, Column: 2, Byte: 32}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + Type: cty.Set(cty.Set(cty.String)), + NestedTargets: reference.Targets{}, + }, + }, + }, + { + "tuple constraint mismatch", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.Tuple([]cty.Type{cty.Bool}), + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `attr = true`, + reference.Targets{}, + }, + { + "tuple empty type-aware", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.Tuple([]cty.Type{cty.String}), + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `attr = []`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + Type: cty.Tuple([]cty.Type{cty.String}), + NestedTargets: reference.Targets{}, + }, + }, + }, + { + "tuple type-aware with invalid element", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.Tuple([]cty.Type{cty.String, cty.String, cty.Number}), + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `attr = ["one", foo, 42224]`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 27, Byte: 26}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + Type: cty.Tuple([]cty.Type{cty.String, cty.String, cty.Number}), + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(0)}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 9, Byte: 8}, + End: hcl.Pos{Line: 1, Column: 14, Byte: 13}, + }, + Type: cty.String, + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(2)}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 21, Byte: 20}, + End: hcl.Pos{Line: 1, Column: 26, Byte: 25}, + }, + Type: cty.Number, + }, + }, + }, + }, + }, + { + "tuple type-aware with extra element", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.Tuple([]cty.Type{cty.String, cty.Number}), + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `attr = ["one", 422, "two"]`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 27, Byte: 26}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + Type: cty.Tuple([]cty.Type{cty.String, cty.Number}), + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(0)}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 9, Byte: 8}, + End: hcl.Pos{Line: 1, Column: 14, Byte: 13}, + }, + Type: cty.String, + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(1)}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 16, Byte: 15}, + End: hcl.Pos{Line: 1, Column: 19, Byte: 18}, + }, + Type: cty.Number, + }, + }, + }, + }, + }, + { + "tuple type-unaware", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.Tuple([]cty.Type{cty.String, cty.String}), + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + ScopeId: lang.ScopeId("test"), + AsReference: true, + }, + }, + }, + `attr = ["one", "two"]`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 22, Byte: 21}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + ScopeId: lang.ScopeId("test"), + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(0)}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 9, Byte: 8}, + End: hcl.Pos{Line: 1, Column: 14, Byte: 13}, + }, + ScopeId: lang.ScopeId("test"), + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(1)}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 16, Byte: 15}, + End: hcl.Pos{Line: 1, Column: 21, Byte: 20}, + }, + ScopeId: lang.ScopeId("test"), + }, + }, + }, + }, + }, + { + "tuple type-aware nested", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.Tuple([]cty.Type{ + cty.Tuple([]cty.Type{ + cty.String, + }), + cty.Tuple([]cty.Type{ + cty.String, + }), + }), + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `attr = [ + ["one"], + ["two"], +] +`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 4, Column: 2, Byte: 32}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + Type: cty.Tuple([]cty.Type{ + cty.Tuple([]cty.Type{cty.String}), + cty.Tuple([]cty.Type{cty.String}), + }), + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(0)}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 11}, + End: hcl.Pos{Line: 2, Column: 10, Byte: 18}, + }, + Type: cty.Tuple([]cty.Type{cty.String}), + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(0)}, + lang.IndexStep{Key: cty.NumberIntVal(0)}, + }, + Type: cty.String, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 2, Column: 4, Byte: 12}, + End: hcl.Pos{Line: 2, Column: 9, Byte: 17}, + }, + }, + }, + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(1)}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 22}, + End: hcl.Pos{Line: 3, Column: 10, Byte: 29}, + }, + Type: cty.Tuple([]cty.Type{cty.String}), + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(1)}, + lang.IndexStep{Key: cty.NumberIntVal(0)}, + }, + Type: cty.String, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 3, Column: 4, Byte: 23}, + End: hcl.Pos{Line: 3, Column: 9, Byte: 28}, + }, + }, + }, + }, + }, + }, + }, + }, + { + "object constraint mismatch", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{ + "foo": cty.String, + }, []string{"foo"}), + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `attr = keyword`, + reference.Targets{}, + }, + { + "object empty type-aware", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{ + "foo": cty.String, + "bar": cty.Number, + }, []string{"foo"}), + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `attr = {}`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{ + "foo": cty.String, + "bar": cty.Number, + }, []string{"foo"}), + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.AttrStep{Name: "bar"}, + }, + Type: cty.Number, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.AttrStep{Name: "foo"}, + }, + Type: cty.String, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + }, + }, + }, + }, + }, + { + "object type-aware with invalid key type", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{ + "foo": cty.String, + "bar": cty.Number, + }, []string{"foo"}), + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `attr = { + 422 = "foo" + bar = 42 +} +`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{ + "foo": cty.String, + "bar": cty.Number, + }, []string{"foo"}), + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 4, Column: 2, Byte: 35}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.AttrStep{Name: "bar"}, + }, + Type: cty.Number, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 25}, + End: hcl.Pos{Line: 3, Column: 11, Byte: 33}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 25}, + End: hcl.Pos{Line: 3, Column: 6, Byte: 28}, + }, + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.AttrStep{Name: "foo"}, + }, + Type: cty.String, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + }, + }, + }, + }, + }, + { + "object type-aware with invalid attribute name", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{ + "foo": cty.String, + "bar": cty.Number, + }, []string{"foo"}), + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `attr = { + fox = "foo" + bar = 42 +} +`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{ + "foo": cty.String, + "bar": cty.Number, + }, []string{"foo"}), + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 4, Column: 2, Byte: 35}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.AttrStep{Name: "bar"}, + }, + Type: cty.Number, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 25}, + End: hcl.Pos{Line: 3, Column: 11, Byte: 33}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 25}, + End: hcl.Pos{Line: 3, Column: 6, Byte: 28}, + }, + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.AttrStep{Name: "foo"}, + }, + Type: cty.String, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + }, + }, + }, + }, + }, + { + "object type-aware with invalid value type", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{ + "foo": cty.String, + "bar": cty.Number, + }, []string{"foo"}), + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `attr = { + foo = 12345 + bar = 42 +} +`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{ + "foo": cty.String, + "bar": cty.Number, + }, []string{"foo"}), + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 4, Column: 2, Byte: 35}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.AttrStep{Name: "bar"}, + }, + Type: cty.Number, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 25}, + End: hcl.Pos{Line: 3, Column: 11, Byte: 33}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 25}, + End: hcl.Pos{Line: 3, Column: 6, Byte: 28}, + }, + }, + }, + }, + }, + }, + { + "object type-unaware", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{ + "foo": cty.String, + "bar": cty.Number, + }, []string{"foo"}), + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + ScopeId: lang.ScopeId("test"), + AsReference: true, + }, + }, + }, + `attr = { + foo = "foo" + bar = 42 +} +`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + ScopeId: lang.ScopeId("test"), + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 4, Column: 2, Byte: 35}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.AttrStep{Name: "bar"}, + }, + ScopeId: lang.ScopeId("test"), + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 25}, + End: hcl.Pos{Line: 3, Column: 11, Byte: 33}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 25}, + End: hcl.Pos{Line: 3, Column: 6, Byte: 28}, + }, + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.AttrStep{Name: "foo"}, + }, + ScopeId: lang.ScopeId("test"), + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 11}, + End: hcl.Pos{Line: 2, Column: 14, Byte: 22}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 11}, + End: hcl.Pos{Line: 2, Column: 6, Byte: 14}, + }, + }, + }, + }, + }, + }, + { + "object nested type-unaware", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Object{ + Attributes: schema.ObjectAttributes{ + "foo": { + Constraint: schema.LiteralType{ + Type: cty.String, + }, + IsOptional: true, + }, + "bar": { + Constraint: schema.Object{ + Attributes: schema.ObjectAttributes{ + "baz": { + Constraint: schema.LiteralType{ + Type: cty.String, + }, + IsRequired: true, + }, + }, + }, + IsRequired: true, + }, + }, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `attr = { + foo = "foo" + bar = { + baz = "noot" + } +} +`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{ + "foo": cty.String, + "bar": cty.Object(map[string]cty.Type{ + "baz": cty.String, + }), + }, []string{"foo"}), + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 6, Column: 2, Byte: 55}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.AttrStep{Name: "bar"}, + }, + Type: cty.Object(map[string]cty.Type{ + "baz": cty.String, + }), + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 25}, + End: hcl.Pos{Line: 5, Column: 4, Byte: 53}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 25}, + End: hcl.Pos{Line: 3, Column: 6, Byte: 28}, + }, + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.AttrStep{Name: "bar"}, + lang.AttrStep{Name: "baz"}, + }, + Type: cty.String, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 4, Column: 5, Byte: 37}, + End: hcl.Pos{Line: 4, Column: 17, Byte: 49}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 4, Column: 5, Byte: 37}, + End: hcl.Pos{Line: 4, Column: 8, Byte: 40}, + }, + }, + }, + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.AttrStep{Name: "foo"}, + }, + Type: cty.String, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 11}, + End: hcl.Pos{Line: 2, Column: 14, Byte: 22}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 11}, + End: hcl.Pos{Line: 2, Column: 6, Byte: 14}, + }, + }, + }, + }, + }, + }, + { + "map constraint mismatch", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.Map(cty.String), + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `attr = keyword`, + reference.Targets{}, + }, + { + "map empty type-aware", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.Map(cty.String), + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `attr = {}`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + Type: cty.Map(cty.String), + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + NestedTargets: reference.Targets{}, + }, + }, + }, + { + "map type-aware with invalid key type", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.Map(cty.Number), + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `attr = { + 422 = "foo" + bar = 42 +} +`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + Type: cty.Map(cty.Number), + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 4, Column: 2, Byte: 35}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.StringVal("bar")}, + }, + Type: cty.Number, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 25}, + End: hcl.Pos{Line: 3, Column: 11, Byte: 33}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 25}, + End: hcl.Pos{Line: 3, Column: 6, Byte: 28}, + }, + }, + }, + }, + }, + }, + { + "map type-aware with multiple items", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.Map(cty.Number), + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `attr = { + fox = 12345 + bar = 42 +} +`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + Type: cty.Map(cty.Number), + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 4, Column: 2, Byte: 35}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.StringVal("bar")}, + }, + Type: cty.Number, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 25}, + End: hcl.Pos{Line: 3, Column: 11, Byte: 33}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 25}, + End: hcl.Pos{Line: 3, Column: 6, Byte: 28}, + }, + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.StringVal("fox")}, + }, + Type: cty.Number, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 11}, + End: hcl.Pos{Line: 2, Column: 14, Byte: 22}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 11}, + End: hcl.Pos{Line: 2, Column: 6, Byte: 14}, + }, + }, + }, + }, + }, + }, + { + "map type-aware with invalid value type", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.Map(cty.Number), + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `attr = { + foo = "foo" + bar = 42 +} +`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + Type: cty.Map(cty.Number), + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 4, Column: 2, Byte: 35}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.StringVal("bar")}, + }, + Type: cty.Number, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 25}, + End: hcl.Pos{Line: 3, Column: 11, Byte: 33}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 25}, + End: hcl.Pos{Line: 3, Column: 6, Byte: 28}, + }, + }, + }, + }, + }, + }, + { + "map type-unaware", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.Map(cty.Number), + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + ScopeId: lang.ScopeId("test"), + AsReference: true, + }, + }, + }, + `attr = { + foo = 12345 + bar = 42 +} +`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + ScopeId: lang.ScopeId("test"), + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 4, Column: 2, Byte: 35}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.StringVal("bar")}, + }, + ScopeId: lang.ScopeId("test"), + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 25}, + End: hcl.Pos{Line: 3, Column: 11, Byte: 33}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 25}, + End: hcl.Pos{Line: 3, Column: 6, Byte: 28}, + }, + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.StringVal("foo")}, + }, + ScopeId: lang.ScopeId("test"), + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 11}, + End: hcl.Pos{Line: 2, Column: 14, Byte: 22}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 11}, + End: hcl.Pos{Line: 2, Column: 6, Byte: 14}, + }, + }, + }, + }, + }, + }, + { + "map nested type-unaware", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.Map(cty.Map(cty.String)), + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `attr = { + foo = { } + bar = { + baz = "noot" + } +} +`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + Type: cty.Map(cty.Map(cty.String)), + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 6, Column: 2, Byte: 55}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.StringVal("bar")}, + }, + Type: cty.Map(cty.String), + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 25}, + End: hcl.Pos{Line: 5, Column: 4, Byte: 53}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 25}, + End: hcl.Pos{Line: 3, Column: 6, Byte: 28}, + }, + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.StringVal("bar")}, + lang.IndexStep{Key: cty.StringVal("baz")}, + }, + Type: cty.String, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 4, Column: 5, Byte: 37}, + End: hcl.Pos{Line: 4, Column: 17, Byte: 49}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 4, Column: 5, Byte: 37}, + End: hcl.Pos{Line: 4, Column: 8, Byte: 40}, + }, + }, + }, + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.StringVal("foo")}, + }, + Type: cty.Map(cty.String), + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 11}, + End: hcl.Pos{Line: 2, Column: 14, Byte: 22}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 11}, + End: hcl.Pos{Line: 2, Column: 6, Byte: 14}, + }, + NestedTargets: reference.Targets{}, + }, + }, + }, + }, + }, + } + for i, tc := range testCases { + t.Run(fmt.Sprintf("%d-%s", i, tc.testName), func(t *testing.T) { + bodySchema := &schema.BodySchema{ + Attributes: tc.attrSchema, + } + + f, diags := hclsyntax.ParseConfig([]byte(tc.cfg), "test.hcl", hcl.InitialPos) + if len(diags) > 0 { + t.Error(diags) + } + d := testPathDecoder(t, &PathContext{ + Schema: bodySchema, + Files: map[string]*hcl.File{ + "test.hcl": f, + }, + }) + + targets, err := d.CollectReferenceTargets() + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(tc.expectedRefTargets, targets, ctydebug.CmpOptions); diff != "" { + t.Fatalf("unexpected targets: %s", diff) + } + }) + } +} + +func TestCollectRefTargets_exprLiteralType_json(t *testing.T) { + testCases := []struct { + testName string + attrSchema map[string]*schema.AttributeSchema + cfg string + expectedRefTargets reference.Targets + }{ + { + "constraint mismatch", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{Type: cty.String}, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `{"attr": true}`, + reference.Targets{}, + }, + { + "bool", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{Type: cty.Bool}, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `{"attr": true}`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + Type: cty.Bool, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 14, Byte: 13}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + }, + }, + }, + { + "string", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{Type: cty.String}, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `{"attr": "foobar"}`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + Type: cty.String, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 18, Byte: 17}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + }, + }, + }, + { + "number", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{Type: cty.Number}, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `{"attr": 42}`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + Type: cty.Number, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 12, Byte: 11}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + }, + }, + }, + { + "list constraint mismatch", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.List(cty.Bool), + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `{"attr": true}`, + reference.Targets{}, + }, + { + "list empty type-aware", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.List(cty.String), + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `{"attr": []}`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 12, Byte: 11}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + Type: cty.List(cty.String), + NestedTargets: reference.Targets{}, + }, + }, + }, + { + "list type-aware with invalid element", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.List(cty.String), + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `{"attr": ["one", 422, "two"]}`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 29, Byte: 28}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + Type: cty.List(cty.String), + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(0)}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 11, Byte: 10}, + End: hcl.Pos{Line: 1, Column: 16, Byte: 15}, + }, + Type: cty.String, + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(2)}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 23, Byte: 22}, + End: hcl.Pos{Line: 1, Column: 28, Byte: 27}, + }, + Type: cty.String, + }, + }, + }, + }, + }, + { + "list type-unaware", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.List(cty.String), + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + ScopeId: lang.ScopeId("test"), + AsReference: true, + }, + }, + }, + `{"attr": ["one", "two"]}`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 24, Byte: 23}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + ScopeId: lang.ScopeId("test"), + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(0)}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 11, Byte: 10}, + End: hcl.Pos{Line: 1, Column: 16, Byte: 15}, + }, + ScopeId: lang.ScopeId("test"), + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(1)}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 18, Byte: 17}, + End: hcl.Pos{Line: 1, Column: 23, Byte: 22}, + }, + ScopeId: lang.ScopeId("test"), + }, + }, + }, + }, + }, + { + "list type-aware nested", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.List(cty.List(cty.String)), + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `{"attr": [ + ["one"], + ["two"] +]}`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 4, Column: 2, Byte: 33}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + Type: cty.List(cty.List(cty.String)), + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(0)}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 13}, + End: hcl.Pos{Line: 2, Column: 10, Byte: 20}, + }, + Type: cty.List(cty.String), + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(0)}, + lang.IndexStep{Key: cty.NumberIntVal(0)}, + }, + Type: cty.String, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 2, Column: 4, Byte: 14}, + End: hcl.Pos{Line: 2, Column: 9, Byte: 19}, + }, + }, + }, + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(1)}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 24}, + End: hcl.Pos{Line: 3, Column: 10, Byte: 31}, + }, + Type: cty.List(cty.String), + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(1)}, + lang.IndexStep{Key: cty.NumberIntVal(0)}, + }, + Type: cty.String, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 3, Column: 4, Byte: 25}, + End: hcl.Pos{Line: 3, Column: 9, Byte: 30}, + }, + }, + }, + }, + }, + }, + }, + }, + { + "set constraint mismatch", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.Set(cty.Bool), + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `{"attr": true}`, + reference.Targets{}, + }, + { + "set empty type-aware", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.Set(cty.String), + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `{"attr": []}`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 12, Byte: 11}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + Type: cty.Set(cty.String), + NestedTargets: reference.Targets{}, + }, + }, + }, + { + "set type-aware with invalid element", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.Set(cty.String), + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `{"attr": ["one", 422, "two"]}`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 29, Byte: 28}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + Type: cty.Set(cty.String), + NestedTargets: reference.Targets{}, + }, + }, + }, + { + "set type-unaware", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.Set(cty.String), + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + ScopeId: lang.ScopeId("test"), + AsReference: true, + }, + }, + }, + `{"attr": ["one", "two"]}`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 24, Byte: 23}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + ScopeId: lang.ScopeId("test"), + NestedTargets: reference.Targets{}, + }, + }, + }, + { + "set type-aware nested", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.Set(cty.Set(cty.String)), + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `{"attr": [ + ["one"], + ["two"] +]}`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 4, Column: 2, Byte: 33}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + Type: cty.Set(cty.Set(cty.String)), + NestedTargets: reference.Targets{}, + }, + }, + }, + { + "tuple constraint mismatch", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.Tuple([]cty.Type{cty.Bool}), + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `{"attr": true}`, + reference.Targets{}, + }, + { + "tuple empty type-aware", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.Tuple([]cty.Type{cty.String}), + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `{"attr": []}`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 12, Byte: 11}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + Type: cty.Tuple([]cty.Type{cty.String}), + NestedTargets: reference.Targets{}, + }, + }, + }, + { + "tuple type-aware with invalid element", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.Tuple([]cty.Type{cty.String, cty.String, cty.Number}), + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `{"attr": ["one", 422, 42223]}`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 29, Byte: 28}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + Type: cty.Tuple([]cty.Type{cty.String, cty.String, cty.Number}), + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(0)}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 11, Byte: 10}, + End: hcl.Pos{Line: 1, Column: 16, Byte: 15}, + }, + Type: cty.String, + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(2)}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 23, Byte: 22}, + End: hcl.Pos{Line: 1, Column: 28, Byte: 27}, + }, + Type: cty.Number, + }, + }, + }, + }, + }, + { + "tuple type-unaware", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.Tuple([]cty.Type{cty.String, cty.String}), + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + ScopeId: lang.ScopeId("test"), + AsReference: true, + }, + }, + }, + `{"attr": ["one", "two"]}`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 24, Byte: 23}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + ScopeId: lang.ScopeId("test"), + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(0)}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 11, Byte: 10}, + End: hcl.Pos{Line: 1, Column: 16, Byte: 15}, + }, + ScopeId: lang.ScopeId("test"), + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(1)}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 18, Byte: 17}, + End: hcl.Pos{Line: 1, Column: 23, Byte: 22}, + }, + ScopeId: lang.ScopeId("test"), + }, + }, + }, + }, + }, + { + "tuple type-aware nested", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.Tuple([]cty.Type{ + cty.Tuple([]cty.Type{cty.String}), + cty.Tuple([]cty.Type{cty.String}), + }), + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `{"attr": [ + ["one"], + ["two"] +]}`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 4, Column: 2, Byte: 33}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + Type: cty.Tuple([]cty.Type{ + cty.Tuple([]cty.Type{cty.String}), + cty.Tuple([]cty.Type{cty.String}), + }), + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(0)}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 13}, + End: hcl.Pos{Line: 2, Column: 10, Byte: 20}, + }, + Type: cty.Tuple([]cty.Type{cty.String}), + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(0)}, + lang.IndexStep{Key: cty.NumberIntVal(0)}, + }, + Type: cty.String, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 2, Column: 4, Byte: 14}, + End: hcl.Pos{Line: 2, Column: 9, Byte: 19}, + }, + }, + }, + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(1)}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 24}, + End: hcl.Pos{Line: 3, Column: 10, Byte: 31}, + }, + Type: cty.Tuple([]cty.Type{cty.String}), + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(1)}, + lang.IndexStep{Key: cty.NumberIntVal(0)}, + }, + Type: cty.String, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 3, Column: 4, Byte: 25}, + End: hcl.Pos{Line: 3, Column: 9, Byte: 30}, + }, + }, + }, + }, + }, + }, + }, + }, + { + "object constraint mismatch", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{ + "foo": cty.String, + }, []string{"foo"}), + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `{"attr": true}`, + reference.Targets{}, + }, + { + "object empty type-aware", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{ + "foo": cty.String, + "bar": cty.Number, + }, []string{"foo"}), + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `{"attr": {}}`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{ + "foo": cty.String, + "bar": cty.Number, + }, []string{"foo"}), + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 12, Byte: 11}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.AttrStep{Name: "bar"}, + }, + Type: cty.Number, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + End: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + }, + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.AttrStep{Name: "foo"}, + }, + Type: cty.String, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + End: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + }, + }, + }, + }, + }, + }, + { + "object type-aware with invalid key type", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{ + "foo": cty.String, + "bar": cty.Number, + }, []string{"foo"}), + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `{"attr": { + "422": "foo", + "bar": 42 +}}`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{ + "foo": cty.String, + "bar": cty.Number, + }, []string{"foo"}), + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 4, Column: 2, Byte: 40}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.AttrStep{Name: "bar"}, + }, + Type: cty.Number, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 29}, + End: hcl.Pos{Line: 3, Column: 12, Byte: 38}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 29}, + End: hcl.Pos{Line: 3, Column: 8, Byte: 34}, + }, + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.AttrStep{Name: "foo"}, + }, + Type: cty.String, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + End: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + }, + }, + }, + }, + }, + }, + { + "object type-aware with invalid attribute name", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{ + "foo": cty.String, + "bar": cty.Number, + }, []string{"foo"}), + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `{"attr": { + "fox": "foo", + "bar": 42 +}}`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{ + "foo": cty.String, + "bar": cty.Number, + }, []string{"foo"}), + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 4, Column: 2, Byte: 40}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.AttrStep{Name: "bar"}, + }, + Type: cty.Number, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 29}, + End: hcl.Pos{Line: 3, Column: 12, Byte: 38}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 29}, + End: hcl.Pos{Line: 3, Column: 8, Byte: 34}, + }, + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.AttrStep{Name: "foo"}, + }, + Type: cty.String, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + End: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + }, + }, + }, + }, + }, + }, + { + "object type-aware with invalid value type", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{ + "foo": cty.String, + "bar": cty.Number, + }, []string{"foo"}), + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `{"attr": { + "foo": 12345, + "bar": 42 +}}`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{ + "foo": cty.String, + "bar": cty.Number, + }, []string{"foo"}), + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 4, Column: 2, Byte: 40}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.AttrStep{Name: "bar"}, + }, + Type: cty.Number, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 29}, + End: hcl.Pos{Line: 3, Column: 12, Byte: 38}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 29}, + End: hcl.Pos{Line: 3, Column: 8, Byte: 34}, + }, + }, + }, + }, + }, + }, + { + "object type-unaware", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{ + "foo": cty.String, + "bar": cty.Number, + }, []string{"foo"}), + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + ScopeId: lang.ScopeId("test"), + AsReference: true, + }, + }, + }, + `{"attr": { + "foo": "foo", + "bar": 42 +}}`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + ScopeId: lang.ScopeId("test"), + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 4, Column: 2, Byte: 40}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.AttrStep{Name: "bar"}, + }, + ScopeId: lang.ScopeId("test"), + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 29}, + End: hcl.Pos{Line: 3, Column: 12, Byte: 38}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 29}, + End: hcl.Pos{Line: 3, Column: 8, Byte: 34}, + }, + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.AttrStep{Name: "foo"}, + }, + ScopeId: lang.ScopeId("test"), + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 13}, + End: hcl.Pos{Line: 2, Column: 15, Byte: 25}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 13}, + End: hcl.Pos{Line: 2, Column: 8, Byte: 18}, + }, + }, + }, + }, + }, + }, + { + "object nested type-unaware", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{ + "foo": cty.String, + "bar": cty.Object(map[string]cty.Type{ + "baz": cty.String, + }), + }, []string{"foo"}), + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `{"attr": { + "foo": "foo", + "bar": { + "baz": "noot" + } +}}`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{ + "foo": cty.String, + "bar": cty.Object(map[string]cty.Type{ + "baz": cty.String, + }), + }, []string{"foo"}), + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 6, Column: 2, Byte: 61}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.AttrStep{Name: "bar"}, + }, + Type: cty.Object(map[string]cty.Type{ + "baz": cty.String, + }), + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 29}, + End: hcl.Pos{Line: 5, Column: 4, Byte: 59}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 29}, + End: hcl.Pos{Line: 3, Column: 8, Byte: 34}, + }, + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.AttrStep{Name: "bar"}, + lang.AttrStep{Name: "baz"}, + }, + Type: cty.String, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 4, Column: 5, Byte: 42}, + End: hcl.Pos{Line: 4, Column: 18, Byte: 55}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 4, Column: 5, Byte: 42}, + End: hcl.Pos{Line: 4, Column: 10, Byte: 47}, + }, + }, + }, + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.AttrStep{Name: "foo"}, + }, + Type: cty.String, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 13}, + End: hcl.Pos{Line: 2, Column: 15, Byte: 25}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 13}, + End: hcl.Pos{Line: 2, Column: 8, Byte: 18}, + }, + }, + }, + }, + }, + }, + { + "map constraint mismatch", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.Map(cty.String), + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `{"attr": true}`, + reference.Targets{}, + }, + { + "map empty type-aware", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.Map(cty.String), + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `{"attr": {}}`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + Type: cty.Map(cty.String), + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 12, Byte: 11}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + NestedTargets: reference.Targets{}, + }, + }, + }, + { + "map type-aware with invalid key type", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.Map(cty.Number), + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `{"attr": { + "422": "foo", + "bar": 42 +}}`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + Type: cty.Map(cty.Number), + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 4, Column: 2, Byte: 40}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.StringVal("bar")}, + }, + Type: cty.Number, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 29}, + End: hcl.Pos{Line: 3, Column: 12, Byte: 38}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 29}, + End: hcl.Pos{Line: 3, Column: 8, Byte: 34}, + }, + }, + }, + }, + }, + }, + { + "map type-aware with multiple items", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.Map(cty.Number), + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `{"attr": { + "fox": 12345, + "bar": 42 +}}`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + Type: cty.Map(cty.Number), + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 4, Column: 2, Byte: 40}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.StringVal("bar")}, + }, + Type: cty.Number, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 29}, + End: hcl.Pos{Line: 3, Column: 12, Byte: 38}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 29}, + End: hcl.Pos{Line: 3, Column: 8, Byte: 34}, + }, + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.StringVal("fox")}, + }, + Type: cty.Number, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 13}, + End: hcl.Pos{Line: 2, Column: 15, Byte: 25}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 13}, + End: hcl.Pos{Line: 2, Column: 8, Byte: 18}, + }, + }, + }, + }, + }, + }, + { + "map type-aware with invalid value type", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.Map(cty.Number), + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `{"attr": { + "foo": "foo", + "bar": 42 +}}`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + Type: cty.Map(cty.Number), + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 4, Column: 2, Byte: 40}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.StringVal("bar")}, + }, + Type: cty.Number, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 29}, + End: hcl.Pos{Line: 3, Column: 12, Byte: 38}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 29}, + End: hcl.Pos{Line: 3, Column: 8, Byte: 34}, + }, + }, + }, + }, + }, + }, + { + "map type-unaware", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.Map(cty.Number), + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + ScopeId: lang.ScopeId("test"), + AsReference: true, + }, + }, + }, + `{"attr": { + "foo": 12345, + "bar": 42 +}}`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + ScopeId: lang.ScopeId("test"), + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 4, Column: 2, Byte: 40}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.StringVal("bar")}, + }, + ScopeId: lang.ScopeId("test"), + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 29}, + End: hcl.Pos{Line: 3, Column: 12, Byte: 38}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 29}, + End: hcl.Pos{Line: 3, Column: 8, Byte: 34}, + }, + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.StringVal("foo")}, + }, + ScopeId: lang.ScopeId("test"), + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 13}, + End: hcl.Pos{Line: 2, Column: 15, Byte: 25}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 13}, + End: hcl.Pos{Line: 2, Column: 8, Byte: 18}, + }, + }, + }, + }, + }, + }, + { + "map nested type-unaware", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.Map(cty.Map(cty.String)), + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `{"attr": { + "foo": { }, + "bar": { + "baz": "noot" + } +}}`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + Type: cty.Map(cty.Map(cty.String)), + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 6, Column: 2, Byte: 61}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.StringVal("bar")}, + }, + Type: cty.Map(cty.String), + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 29}, + End: hcl.Pos{Line: 5, Column: 4, Byte: 59}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 29}, + End: hcl.Pos{Line: 3, Column: 8, Byte: 34}, + }, + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.StringVal("bar")}, + lang.IndexStep{Key: cty.StringVal("baz")}, + }, + Type: cty.String, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 4, Column: 5, Byte: 42}, + End: hcl.Pos{Line: 4, Column: 18, Byte: 55}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 4, Column: 5, Byte: 42}, + End: hcl.Pos{Line: 4, Column: 10, Byte: 47}, + }, + }, + }, + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.StringVal("foo")}, + }, + Type: cty.Map(cty.String), + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 13}, + End: hcl.Pos{Line: 2, Column: 15, Byte: 25}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 13}, + End: hcl.Pos{Line: 2, Column: 8, Byte: 18}, + }, + NestedTargets: reference.Targets{}, + }, + }, + }, + }, + }, + } + for i, tc := range testCases { + t.Run(fmt.Sprintf("%d-%s", i, tc.testName), func(t *testing.T) { + bodySchema := &schema.BodySchema{ + Attributes: tc.attrSchema, + } + + f, diags := json.ParseWithStartPos([]byte(tc.cfg), "test.hcl.json", hcl.InitialPos) + if len(diags) > 0 { + t.Error(diags) + } + d := testPathDecoder(t, &PathContext{ + Schema: bodySchema, + Files: map[string]*hcl.File{ + "test.hcl.json": f, + }, + }) + + targets, err := d.CollectReferenceTargets() + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(tc.expectedRefTargets, targets, ctydebug.CmpOptions); diff != "" { + t.Fatalf("unexpected targets: %s", diff) + } + }) + } +} diff --git a/decoder/expr_map_ref_targets.go b/decoder/expr_map_ref_targets.go index 0e5b0876..63ec33d3 100644 --- a/decoder/expr_map_ref_targets.go +++ b/decoder/expr_map_ref_targets.go @@ -2,34 +2,35 @@ package decoder import ( "context" + "sort" "github.com/hashicorp/hcl-lang/lang" "github.com/hashicorp/hcl-lang/reference" "github.com/hashicorp/hcl-lang/schema" - "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/hashicorp/hcl/v2" "github.com/zclconf/go-cty/cty" ) func (m Map) ReferenceTargets(ctx context.Context, targetCtx *TargetContext) reference.Targets { - eType, ok := m.expr.(*hclsyntax.ObjectConsExpr) - if !ok { + items, diags := hcl.ExprMap(m.expr) + if diags.HasErrors() { return reference.Targets{} } - if len(eType.Items) == 0 || m.cons.Elem == nil { + if m.cons.Elem == nil { return reference.Targets{} } elemTargets := make(reference.Targets, 0) - for _, item := range eType.Items { - keyName, _, ok := rawObjectKey(item.KeyExpr) + for _, item := range items { + keyName, _, ok := rawObjectKey(item.Key) if !ok { // avoid collecting item w/ invalid key continue } - expr := newExpression(m.pathCtx, item.ValueExpr, m.cons.Elem) + expr := newExpression(m.pathCtx, item.Value, m.cons.Elem) if e, ok := expr.(ReferenceTargetsExpression); ok { if targetCtx == nil { // collect any targets inside the expression @@ -39,6 +40,10 @@ func (m Map) ReferenceTargets(ctx context.Context, targetCtx *TargetContext) ref } elemCtx := targetCtx.Copy() + + elemCtx.ParentDefRangePtr = item.Key.Range().Ptr() + elemCtx.ParentRangePtr = hcl.RangeBetween(item.Key.Range(), item.Value.Range()).Ptr() + elemCtx.ParentAddress = append(elemCtx.ParentAddress, lang.IndexStep{ Key: cty.StringVal(keyName), }) @@ -52,11 +57,20 @@ func (m Map) ReferenceTargets(ctx context.Context, targetCtx *TargetContext) ref } } + sort.Sort(elemTargets) + targets := make(reference.Targets, 0) if targetCtx != nil { // collect target for the whole map + var rangePtr *hcl.Range + if targetCtx.ParentRangePtr != nil { + rangePtr = targetCtx.ParentRangePtr + } else { + rangePtr = m.expr.Range().Ptr() + } + // type-aware elemCons, ok := m.cons.Elem.(schema.TypeAwareConstraint) if targetCtx.AsExprType && ok { @@ -67,7 +81,8 @@ func (m Map) ReferenceTargets(ctx context.Context, targetCtx *TargetContext) ref Name: targetCtx.FriendlyName, Type: cty.Map(elemType), ScopeId: targetCtx.ScopeId, - RangePtr: m.expr.Range().Ptr(), + RangePtr: rangePtr, + DefRangePtr: targetCtx.ParentDefRangePtr, NestedTargets: elemTargets, LocalAddr: targetCtx.ParentLocalAddress, TargetableFromRangePtr: targetCtx.TargetableFromRangePtr, @@ -81,7 +96,8 @@ func (m Map) ReferenceTargets(ctx context.Context, targetCtx *TargetContext) ref Addr: targetCtx.ParentAddress, Name: targetCtx.FriendlyName, ScopeId: targetCtx.ScopeId, - RangePtr: m.expr.Range().Ptr(), + RangePtr: rangePtr, + DefRangePtr: targetCtx.ParentDefRangePtr, NestedTargets: elemTargets, LocalAddr: targetCtx.ParentLocalAddress, TargetableFromRangePtr: targetCtx.TargetableFromRangePtr, diff --git a/decoder/expr_map_ref_targets_test.go b/decoder/expr_map_ref_targets_test.go new file mode 100644 index 00000000..56fa332a --- /dev/null +++ b/decoder/expr_map_ref_targets_test.go @@ -0,0 +1,1073 @@ +package decoder + +import ( + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/hcl-lang/lang" + "github.com/hashicorp/hcl-lang/reference" + "github.com/hashicorp/hcl-lang/schema" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/hashicorp/hcl/v2/json" + "github.com/zclconf/go-cty-debug/ctydebug" + "github.com/zclconf/go-cty/cty" +) + +func TestCollectRefTargets_exprMap_hcl(t *testing.T) { + testCases := []struct { + testName string + attrSchema map[string]*schema.AttributeSchema + cfg string + expectedRefTargets reference.Targets + }{ + { + "constraint mismatch", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Map{ + Elem: schema.Keyword{ + Keyword: "keyword", + }, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `attr = keyword`, + reference.Targets{}, + }, + { + "no collectable constraint", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Map{ + Elem: schema.Keyword{ + Keyword: "keyword", + }, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `attr = { foo = keyword }`, + reference.Targets{}, + }, + { + "addressable reference only", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Map{ + Elem: schema.Reference{ + Address: &schema.ReferenceAddrSchema{ + ScopeId: lang.ScopeId("test"), + }, + }, + }, + IsOptional: true, + }, + }, + `attr = { + foo = foo +}`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "foo"}, + }, + ScopeId: lang.ScopeId("test"), + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 2, Column: 9, Byte: 17}, + End: hcl.Pos{Line: 2, Column: 12, Byte: 20}, + }, + }, + }, + }, + { + "empty type-aware", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Map{ + Elem: schema.LiteralType{ + Type: cty.String, + }, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `attr = {}`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + Type: cty.Map(cty.String), + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + NestedTargets: reference.Targets{}, + }, + }, + }, + { + "type-aware with invalid key type", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Map{ + Elem: schema.LiteralType{ + Type: cty.Number, + }, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `attr = { + 422 = "foo" + bar = 42 +} +`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + Type: cty.Map(cty.Number), + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 4, Column: 2, Byte: 35}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.StringVal("bar")}, + }, + Type: cty.Number, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 25}, + End: hcl.Pos{Line: 3, Column: 11, Byte: 33}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 25}, + End: hcl.Pos{Line: 3, Column: 6, Byte: 28}, + }, + }, + }, + }, + }, + }, + { + "type-aware with multiple items", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Map{ + Elem: schema.LiteralType{ + Type: cty.Number, + }, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `attr = { + fox = 12345 + bar = 42 +} +`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + Type: cty.Map(cty.Number), + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 4, Column: 2, Byte: 35}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.StringVal("bar")}, + }, + Type: cty.Number, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 25}, + End: hcl.Pos{Line: 3, Column: 11, Byte: 33}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 25}, + End: hcl.Pos{Line: 3, Column: 6, Byte: 28}, + }, + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.StringVal("fox")}, + }, + Type: cty.Number, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 11}, + End: hcl.Pos{Line: 2, Column: 14, Byte: 22}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 11}, + End: hcl.Pos{Line: 2, Column: 6, Byte: 14}, + }, + }, + }, + }, + }, + }, + { + "type-aware with invalid value type", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Map{ + Elem: schema.LiteralType{ + Type: cty.Number, + }, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `attr = { + foo = "foo" + bar = 42 +} +`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + Type: cty.Map(cty.Number), + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 4, Column: 2, Byte: 35}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.StringVal("bar")}, + }, + Type: cty.Number, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 25}, + End: hcl.Pos{Line: 3, Column: 11, Byte: 33}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 25}, + End: hcl.Pos{Line: 3, Column: 6, Byte: 28}, + }, + }, + }, + }, + }, + }, + { + "type-unaware", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Map{ + Elem: schema.LiteralType{ + Type: cty.Number, + }, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + ScopeId: lang.ScopeId("test"), + AsReference: true, + }, + }, + }, + `attr = { + foo = 12345 + bar = 42 +} +`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + ScopeId: lang.ScopeId("test"), + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 4, Column: 2, Byte: 35}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.StringVal("bar")}, + }, + ScopeId: lang.ScopeId("test"), + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 25}, + End: hcl.Pos{Line: 3, Column: 11, Byte: 33}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 25}, + End: hcl.Pos{Line: 3, Column: 6, Byte: 28}, + }, + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.StringVal("foo")}, + }, + ScopeId: lang.ScopeId("test"), + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 11}, + End: hcl.Pos{Line: 2, Column: 14, Byte: 22}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 11}, + End: hcl.Pos{Line: 2, Column: 6, Byte: 14}, + }, + }, + }, + }, + }, + }, + { + "nested type-unaware", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Map{ + Elem: schema.Map{ + Elem: schema.LiteralType{ + Type: cty.String, + }, + }, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `attr = { + foo = { } + bar = { + baz = "noot" + } +} +`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + Type: cty.Map(cty.Map(cty.String)), + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 6, Column: 2, Byte: 55}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.StringVal("bar")}, + }, + Type: cty.Map(cty.String), + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 25}, + End: hcl.Pos{Line: 5, Column: 4, Byte: 53}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 25}, + End: hcl.Pos{Line: 3, Column: 6, Byte: 28}, + }, + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.StringVal("bar")}, + lang.IndexStep{Key: cty.StringVal("baz")}, + }, + Type: cty.String, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 4, Column: 5, Byte: 37}, + End: hcl.Pos{Line: 4, Column: 17, Byte: 49}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 4, Column: 5, Byte: 37}, + End: hcl.Pos{Line: 4, Column: 8, Byte: 40}, + }, + }, + }, + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.StringVal("foo")}, + }, + Type: cty.Map(cty.String), + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 11}, + End: hcl.Pos{Line: 2, Column: 14, Byte: 22}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 11}, + End: hcl.Pos{Line: 2, Column: 6, Byte: 14}, + }, + NestedTargets: reference.Targets{}, + }, + }, + }, + }, + }, + } + for i, tc := range testCases { + t.Run(fmt.Sprintf("%d-%s", i, tc.testName), func(t *testing.T) { + bodySchema := &schema.BodySchema{ + Attributes: tc.attrSchema, + } + + f, diags := hclsyntax.ParseConfig([]byte(tc.cfg), "test.hcl", hcl.InitialPos) + if len(diags) > 0 { + t.Error(diags) + } + d := testPathDecoder(t, &PathContext{ + Schema: bodySchema, + Files: map[string]*hcl.File{ + "test.hcl": f, + }, + }) + + targets, err := d.CollectReferenceTargets() + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(tc.expectedRefTargets, targets, ctydebug.CmpOptions); diff != "" { + t.Fatalf("unexpected targets: %s", diff) + } + }) + } +} + +func TestCollectRefTargets_exprMap_json(t *testing.T) { + testCases := []struct { + testName string + attrSchema map[string]*schema.AttributeSchema + cfg string + expectedRefTargets reference.Targets + }{ + { + "constraint mismatch", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Map{ + Elem: schema.Keyword{ + Keyword: "keyword", + }, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `{"attr": true}`, + reference.Targets{}, + }, + { + "no collectable constraint", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Map{ + Elem: schema.Keyword{ + Keyword: "keyword", + }, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `{"attr": { "foo": "keyword" }}`, + reference.Targets{}, + }, + { + "addressable reference only", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Map{ + Elem: schema.Reference{ + Address: &schema.ReferenceAddrSchema{ + ScopeId: lang.ScopeId("test"), + }, + }, + }, + IsOptional: true, + }, + }, + `{"attr": { + "foo": "foo" +}}`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "foo"}, + }, + ScopeId: lang.ScopeId("test"), + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 2, Column: 11, Byte: 21}, + End: hcl.Pos{Line: 2, Column: 14, Byte: 24}, + }, + }, + }, + }, + { + "empty type-aware", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Map{ + Elem: schema.LiteralType{ + Type: cty.String, + }, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `{"attr": {}}`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + Type: cty.Map(cty.String), + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 12, Byte: 11}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + NestedTargets: reference.Targets{}, + }, + }, + }, + { + "type-aware with invalid key type", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Map{ + Elem: schema.LiteralType{ + Type: cty.Number, + }, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `{"attr": { + "422": "foo", + "bar": 42 +}}`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + Type: cty.Map(cty.Number), + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 4, Column: 2, Byte: 40}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.StringVal("bar")}, + }, + Type: cty.Number, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 29}, + End: hcl.Pos{Line: 3, Column: 12, Byte: 38}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 29}, + End: hcl.Pos{Line: 3, Column: 8, Byte: 34}, + }, + }, + }, + }, + }, + }, + { + "type-aware with multiple items", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Map{ + Elem: schema.LiteralType{ + Type: cty.Number, + }, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `{"attr": { + "fox": 12345, + "bar": 42 +}}`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + Type: cty.Map(cty.Number), + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 4, Column: 2, Byte: 40}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.StringVal("bar")}, + }, + Type: cty.Number, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 29}, + End: hcl.Pos{Line: 3, Column: 12, Byte: 38}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 29}, + End: hcl.Pos{Line: 3, Column: 8, Byte: 34}, + }, + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.StringVal("fox")}, + }, + Type: cty.Number, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 13}, + End: hcl.Pos{Line: 2, Column: 15, Byte: 25}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 13}, + End: hcl.Pos{Line: 2, Column: 8, Byte: 18}, + }, + }, + }, + }, + }, + }, + { + "type-aware with invalid value type", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Map{ + Elem: schema.LiteralType{ + Type: cty.Number, + }, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `{"attr": { + "foo": "foo", + "bar": 42 +}}`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + Type: cty.Map(cty.Number), + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 4, Column: 2, Byte: 40}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.StringVal("bar")}, + }, + Type: cty.Number, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 29}, + End: hcl.Pos{Line: 3, Column: 12, Byte: 38}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 29}, + End: hcl.Pos{Line: 3, Column: 8, Byte: 34}, + }, + }, + }, + }, + }, + }, + { + "type-unaware", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Map{ + Elem: schema.LiteralType{ + Type: cty.Number, + }, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + ScopeId: lang.ScopeId("test"), + AsReference: true, + }, + }, + }, + `{"attr": { + "foo": 12345, + "bar": 42 +}}`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + ScopeId: lang.ScopeId("test"), + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 4, Column: 2, Byte: 40}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.StringVal("bar")}, + }, + ScopeId: lang.ScopeId("test"), + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 29}, + End: hcl.Pos{Line: 3, Column: 12, Byte: 38}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 29}, + End: hcl.Pos{Line: 3, Column: 8, Byte: 34}, + }, + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.StringVal("foo")}, + }, + ScopeId: lang.ScopeId("test"), + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 13}, + End: hcl.Pos{Line: 2, Column: 15, Byte: 25}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 13}, + End: hcl.Pos{Line: 2, Column: 8, Byte: 18}, + }, + }, + }, + }, + }, + }, + { + "nested type-unaware", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Map{ + Elem: schema.Map{ + Elem: schema.LiteralType{ + Type: cty.String, + }, + }, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `{"attr": { + "foo": { }, + "bar": { + "baz": "noot" + } +}}`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + Type: cty.Map(cty.Map(cty.String)), + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 6, Column: 2, Byte: 61}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.StringVal("bar")}, + }, + Type: cty.Map(cty.String), + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 29}, + End: hcl.Pos{Line: 5, Column: 4, Byte: 59}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 29}, + End: hcl.Pos{Line: 3, Column: 8, Byte: 34}, + }, + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.StringVal("bar")}, + lang.IndexStep{Key: cty.StringVal("baz")}, + }, + Type: cty.String, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 4, Column: 5, Byte: 42}, + End: hcl.Pos{Line: 4, Column: 18, Byte: 55}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 4, Column: 5, Byte: 42}, + End: hcl.Pos{Line: 4, Column: 10, Byte: 47}, + }, + }, + }, + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.StringVal("foo")}, + }, + Type: cty.Map(cty.String), + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 13}, + End: hcl.Pos{Line: 2, Column: 15, Byte: 25}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 13}, + End: hcl.Pos{Line: 2, Column: 8, Byte: 18}, + }, + NestedTargets: reference.Targets{}, + }, + }, + }, + }, + }, + } + for i, tc := range testCases { + t.Run(fmt.Sprintf("%d-%s", i, tc.testName), func(t *testing.T) { + bodySchema := &schema.BodySchema{ + Attributes: tc.attrSchema, + } + + f, diags := json.ParseWithStartPos([]byte(tc.cfg), "test.hcl.json", hcl.InitialPos) + if len(diags) > 0 { + t.Error(diags) + } + d := testPathDecoder(t, &PathContext{ + Schema: bodySchema, + Files: map[string]*hcl.File{ + "test.hcl.json": f, + }, + }) + + targets, err := d.CollectReferenceTargets() + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(tc.expectedRefTargets, targets, ctydebug.CmpOptions); diff != "" { + t.Fatalf("unexpected targets: %s", diff) + } + }) + } +} diff --git a/decoder/expr_object_ref_targets.go b/decoder/expr_object_ref_targets.go index c1a6f06b..998cc976 100644 --- a/decoder/expr_object_ref_targets.go +++ b/decoder/expr_object_ref_targets.go @@ -5,36 +5,52 @@ import ( "github.com/hashicorp/hcl-lang/lang" "github.com/hashicorp/hcl-lang/reference" + "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" "github.com/zclconf/go-cty/cty" ) func (obj Object) ReferenceTargets(ctx context.Context, targetCtx *TargetContext) reference.Targets { - eType, ok := obj.expr.(*hclsyntax.ObjectConsExpr) - if !ok { + items, diags := hcl.ExprMap(obj.expr) + if diags.HasErrors() { return reference.Targets{} } - if len(eType.Items) == 0 || len(obj.cons.Attributes) == 0 { + if len(obj.cons.Attributes) == 0 { return reference.Targets{} } attrTargets := make(reference.Targets, 0) - for _, item := range eType.Items { - keyName, _, ok := rawObjectKey(item.KeyExpr) + declaredAttributes := make(map[string]hcl.KeyValuePair, 0) + for _, item := range items { + keyName, _, ok := rawObjectKey(item.Key) if !ok { // avoid collecting item w/ invalid key continue } - aSchema, ok := obj.cons.Attributes[keyName] + _, ok = obj.cons.Attributes[keyName] if !ok { // avoid collecting for unknown attribute continue } - expr := newExpression(obj.pathCtx, item.ValueExpr, aSchema.Constraint) + declaredAttributes[keyName] = item + } + + attrNames := sortedAttributeNames(obj.cons.Attributes) + for _, name := range attrNames { + var valueExpr hcl.Expression + item, attrDeclared := declaredAttributes[name] + if attrDeclared { + valueExpr = item.Value + } else { + valueExpr = newEmptyExpressionAtPos(obj.expr.Range().Filename, obj.expr.Range().Start) + } + + aSchema := obj.cons.Attributes[name] + expr := newExpression(obj.pathCtx, valueExpr, aSchema.Constraint) if e, ok := expr.(ReferenceTargetsExpression); ok { if targetCtx == nil { // collect any targets inside the expression @@ -44,26 +60,50 @@ func (obj Object) ReferenceTargets(ctx context.Context, targetCtx *TargetContext } elemCtx := targetCtx.Copy() - elemCtx.ParentAddress = append(elemCtx.ParentAddress, lang.IndexStep{ - Key: cty.StringVal(keyName), - }) - if elemCtx.ParentLocalAddress != nil { - elemCtx.ParentLocalAddress = append(elemCtx.ParentLocalAddress, lang.IndexStep{ - Key: cty.StringVal(keyName), + + if attrDeclared { + elemCtx.ParentDefRangePtr = item.Key.Range().Ptr() + elemCtx.ParentRangePtr = hcl.RangeBetween(item.Key.Range(), item.Value.Range()).Ptr() + } + + if hclsyntax.ValidIdentifier(name) { + // Prefer simpler syntax - e.g. myobj.attribute if possible + elemCtx.ParentAddress = append(elemCtx.ParentAddress, lang.AttrStep{ + Name: name, + }) + if elemCtx.ParentLocalAddress != nil { + elemCtx.ParentLocalAddress = append(elemCtx.ParentLocalAddress, lang.AttrStep{ + Name: name, + }) + } + } else { + // Fall back to indexing syntax - e.g. myobj["attr-foo"] + elemCtx.ParentAddress = append(elemCtx.ParentAddress, lang.IndexStep{ + Key: cty.StringVal(name), }) + if elemCtx.ParentLocalAddress != nil { + elemCtx.ParentLocalAddress = append(elemCtx.ParentLocalAddress, lang.IndexStep{ + Key: cty.StringVal(name), + }) + } } attrTargets = append(attrTargets, e.ReferenceTargets(ctx, elemCtx)...) } } - // TODO: targets for undeclared attributes w/out range - targets := make(reference.Targets, 0) if targetCtx != nil { // collect target for the whole object + var rangePtr *hcl.Range + if targetCtx.ParentRangePtr != nil { + rangePtr = targetCtx.ParentRangePtr + } else { + rangePtr = obj.expr.Range().Ptr() + } + // type-aware if targetCtx.AsExprType { objType, ok := obj.cons.ConstraintType() @@ -73,7 +113,8 @@ func (obj Object) ReferenceTargets(ctx context.Context, targetCtx *TargetContext Name: targetCtx.FriendlyName, Type: objType, ScopeId: targetCtx.ScopeId, - RangePtr: obj.expr.Range().Ptr(), + DefRangePtr: targetCtx.ParentDefRangePtr, + RangePtr: rangePtr, NestedTargets: attrTargets, LocalAddr: targetCtx.ParentLocalAddress, TargetableFromRangePtr: targetCtx.TargetableFromRangePtr, @@ -87,7 +128,8 @@ func (obj Object) ReferenceTargets(ctx context.Context, targetCtx *TargetContext Addr: targetCtx.ParentAddress, Name: targetCtx.FriendlyName, ScopeId: targetCtx.ScopeId, - RangePtr: obj.expr.Range().Ptr(), + DefRangePtr: targetCtx.ParentDefRangePtr, + RangePtr: rangePtr, NestedTargets: attrTargets, LocalAddr: targetCtx.ParentLocalAddress, TargetableFromRangePtr: targetCtx.TargetableFromRangePtr, diff --git a/decoder/expr_object_ref_targets_test.go b/decoder/expr_object_ref_targets_test.go new file mode 100644 index 00000000..63fa13b8 --- /dev/null +++ b/decoder/expr_object_ref_targets_test.go @@ -0,0 +1,1345 @@ +package decoder + +import ( + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/hcl-lang/lang" + "github.com/hashicorp/hcl-lang/reference" + "github.com/hashicorp/hcl-lang/schema" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/hashicorp/hcl/v2/json" + "github.com/zclconf/go-cty-debug/ctydebug" + "github.com/zclconf/go-cty/cty" +) + +func TestCollectRefTargets_exprObject_hcl(t *testing.T) { + testCases := []struct { + testName string + attrSchema map[string]*schema.AttributeSchema + cfg string + expectedRefTargets reference.Targets + }{ + { + "constraint mismatch", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Object{ + Attributes: schema.ObjectAttributes{ + "foo": { + Constraint: schema.Keyword{ + Keyword: "keyword", + }, + IsOptional: true, + }, + }, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `attr = keyword`, + reference.Targets{}, + }, + { + "no collectable constraint", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Object{ + Attributes: schema.ObjectAttributes{ + "foo": { + Constraint: schema.Keyword{ + Keyword: "keyword", + }, + IsOptional: true, + }, + }, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `attr = { foo = keyword }`, + reference.Targets{}, + }, + { + "addressable reference only", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Object{ + Attributes: schema.ObjectAttributes{ + "foo": { + Constraint: schema.Reference{ + Address: &schema.ReferenceAddrSchema{ + ScopeId: lang.ScopeId("test"), + }, + }, + IsOptional: true, + }, + }, + }, + IsOptional: true, + }, + }, + `attr = { + foo = foo +}`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "foo"}, + }, + ScopeId: lang.ScopeId("test"), + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 2, Column: 9, Byte: 17}, + End: hcl.Pos{Line: 2, Column: 12, Byte: 20}, + }, + }, + }, + }, + { + "empty type-aware", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Object{ + Attributes: schema.ObjectAttributes{ + "foo": { + Constraint: schema.LiteralType{ + Type: cty.String, + }, + IsOptional: true, + }, + "bar": { + Constraint: schema.LiteralType{ + Type: cty.Number, + }, + IsRequired: true, + }, + }, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `attr = {}`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{ + "foo": cty.String, + "bar": cty.Number, + }, []string{"foo"}), + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.AttrStep{Name: "bar"}, + }, + Type: cty.Number, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.AttrStep{Name: "foo"}, + }, + Type: cty.String, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + }, + }, + }, + }, + }, + { + "type-aware with invalid key type", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Object{ + Attributes: schema.ObjectAttributes{ + "foo": { + Constraint: schema.LiteralType{ + Type: cty.String, + }, + IsOptional: true, + }, + "bar": { + Constraint: schema.LiteralType{ + Type: cty.Number, + }, + IsRequired: true, + }, + }, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `attr = { + 422 = "foo" + bar = 42 +} +`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{ + "foo": cty.String, + "bar": cty.Number, + }, []string{"foo"}), + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 4, Column: 2, Byte: 35}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.AttrStep{Name: "bar"}, + }, + Type: cty.Number, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 25}, + End: hcl.Pos{Line: 3, Column: 11, Byte: 33}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 25}, + End: hcl.Pos{Line: 3, Column: 6, Byte: 28}, + }, + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.AttrStep{Name: "foo"}, + }, + Type: cty.String, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + }, + }, + }, + }, + }, + { + "type-aware with invalid attribute name", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Object{ + Attributes: schema.ObjectAttributes{ + "foo": { + Constraint: schema.LiteralType{ + Type: cty.String, + }, + IsOptional: true, + }, + "bar": { + Constraint: schema.LiteralType{ + Type: cty.Number, + }, + IsRequired: true, + }, + }, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `attr = { + fox = "foo" + bar = 42 +} +`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{ + "foo": cty.String, + "bar": cty.Number, + }, []string{"foo"}), + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 4, Column: 2, Byte: 35}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.AttrStep{Name: "bar"}, + }, + Type: cty.Number, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 25}, + End: hcl.Pos{Line: 3, Column: 11, Byte: 33}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 25}, + End: hcl.Pos{Line: 3, Column: 6, Byte: 28}, + }, + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.AttrStep{Name: "foo"}, + }, + Type: cty.String, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + }, + }, + }, + }, + }, + { + "type-aware with invalid value type", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Object{ + Attributes: schema.ObjectAttributes{ + "foo": { + Constraint: schema.LiteralType{ + Type: cty.String, + }, + IsOptional: true, + }, + "bar": { + Constraint: schema.LiteralType{ + Type: cty.Number, + }, + IsRequired: true, + }, + }, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `attr = { + foo = 12345 + bar = 42 +} +`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{ + "foo": cty.String, + "bar": cty.Number, + }, []string{"foo"}), + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 4, Column: 2, Byte: 35}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.AttrStep{Name: "bar"}, + }, + Type: cty.Number, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 25}, + End: hcl.Pos{Line: 3, Column: 11, Byte: 33}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 25}, + End: hcl.Pos{Line: 3, Column: 6, Byte: 28}, + }, + }, + }, + }, + }, + }, + { + "type-unaware", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Object{ + Attributes: schema.ObjectAttributes{ + "foo": { + Constraint: schema.LiteralType{ + Type: cty.String, + }, + IsOptional: true, + }, + "bar": { + Constraint: schema.LiteralType{ + Type: cty.Number, + }, + IsRequired: true, + }, + }, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + ScopeId: lang.ScopeId("test"), + AsReference: true, + }, + }, + }, + `attr = { + foo = "foo" + bar = 42 +} +`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + ScopeId: lang.ScopeId("test"), + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 4, Column: 2, Byte: 35}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.AttrStep{Name: "bar"}, + }, + ScopeId: lang.ScopeId("test"), + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 25}, + End: hcl.Pos{Line: 3, Column: 11, Byte: 33}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 25}, + End: hcl.Pos{Line: 3, Column: 6, Byte: 28}, + }, + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.AttrStep{Name: "foo"}, + }, + ScopeId: lang.ScopeId("test"), + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 11}, + End: hcl.Pos{Line: 2, Column: 14, Byte: 22}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 11}, + End: hcl.Pos{Line: 2, Column: 6, Byte: 14}, + }, + }, + }, + }, + }, + }, + { + "nested type-unaware", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Object{ + Attributes: schema.ObjectAttributes{ + "foo": { + Constraint: schema.LiteralType{ + Type: cty.String, + }, + IsOptional: true, + }, + "bar": { + Constraint: schema.Object{ + Attributes: schema.ObjectAttributes{ + "baz": { + Constraint: schema.LiteralType{ + Type: cty.String, + }, + IsRequired: true, + }, + }, + }, + IsRequired: true, + }, + }, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `attr = { + foo = "foo" + bar = { + baz = "noot" + } +} +`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{ + "foo": cty.String, + "bar": cty.Object(map[string]cty.Type{ + "baz": cty.String, + }), + }, []string{"foo"}), + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 6, Column: 2, Byte: 55}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.AttrStep{Name: "bar"}, + }, + Type: cty.Object(map[string]cty.Type{ + "baz": cty.String, + }), + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 25}, + End: hcl.Pos{Line: 5, Column: 4, Byte: 53}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 25}, + End: hcl.Pos{Line: 3, Column: 6, Byte: 28}, + }, + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.AttrStep{Name: "bar"}, + lang.AttrStep{Name: "baz"}, + }, + Type: cty.String, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 4, Column: 5, Byte: 37}, + End: hcl.Pos{Line: 4, Column: 17, Byte: 49}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 4, Column: 5, Byte: 37}, + End: hcl.Pos{Line: 4, Column: 8, Byte: 40}, + }, + }, + }, + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.AttrStep{Name: "foo"}, + }, + Type: cty.String, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 11}, + End: hcl.Pos{Line: 2, Column: 14, Byte: 22}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 11}, + End: hcl.Pos{Line: 2, Column: 6, Byte: 14}, + }, + }, + }, + }, + }, + }, + } + for i, tc := range testCases { + t.Run(fmt.Sprintf("%d-%s", i, tc.testName), func(t *testing.T) { + bodySchema := &schema.BodySchema{ + Attributes: tc.attrSchema, + } + + f, diags := hclsyntax.ParseConfig([]byte(tc.cfg), "test.hcl", hcl.InitialPos) + if len(diags) > 0 { + t.Error(diags) + } + d := testPathDecoder(t, &PathContext{ + Schema: bodySchema, + Files: map[string]*hcl.File{ + "test.hcl": f, + }, + }) + + targets, err := d.CollectReferenceTargets() + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(tc.expectedRefTargets, targets, ctydebug.CmpOptions); diff != "" { + t.Fatalf("unexpected targets: %s", diff) + } + }) + } +} + +func TestCollectRefTargets_exprObject_json(t *testing.T) { + testCases := []struct { + testName string + attrSchema map[string]*schema.AttributeSchema + cfg string + expectedRefTargets reference.Targets + }{ + { + "constraint mismatch", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Object{ + Attributes: schema.ObjectAttributes{ + "foo": { + Constraint: schema.Keyword{ + Keyword: "keyword", + }, + IsOptional: true, + }, + }, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `{"attr": true}`, + reference.Targets{}, + }, + { + "no collectable constraint", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Object{ + Attributes: schema.ObjectAttributes{ + "foo": { + Constraint: schema.Keyword{ + Keyword: "keyword", + }, + IsOptional: true, + }, + }, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `{"attr": { "foo": "keyword" }}`, + reference.Targets{}, + }, + { + "addressable reference only", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Object{ + Attributes: schema.ObjectAttributes{ + "foo": { + Constraint: schema.Reference{ + Address: &schema.ReferenceAddrSchema{ + ScopeId: lang.ScopeId("test"), + }, + }, + IsOptional: true, + }, + }, + }, + IsOptional: true, + }, + }, + `{"attr": { + "foo": "foo" +}}`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "foo"}, + }, + ScopeId: lang.ScopeId("test"), + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 2, Column: 11, Byte: 21}, + End: hcl.Pos{Line: 2, Column: 14, Byte: 24}, + }, + }, + }, + }, + { + "empty type-aware", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Object{ + Attributes: schema.ObjectAttributes{ + "foo": { + Constraint: schema.LiteralType{ + Type: cty.String, + }, + IsOptional: true, + }, + "bar": { + Constraint: schema.LiteralType{ + Type: cty.Number, + }, + IsRequired: true, + }, + }, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `{"attr": {}}`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{ + "foo": cty.String, + "bar": cty.Number, + }, []string{"foo"}), + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 12, Byte: 11}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.AttrStep{Name: "bar"}, + }, + Type: cty.Number, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + End: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + }, + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.AttrStep{Name: "foo"}, + }, + Type: cty.String, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + End: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + }, + }, + }, + }, + }, + }, + { + "type-aware with invalid key type", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Object{ + Attributes: schema.ObjectAttributes{ + "foo": { + Constraint: schema.LiteralType{ + Type: cty.String, + }, + IsOptional: true, + }, + "bar": { + Constraint: schema.LiteralType{ + Type: cty.Number, + }, + IsRequired: true, + }, + }, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `{"attr": { + "422": "foo", + "bar": 42 +}}`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{ + "foo": cty.String, + "bar": cty.Number, + }, []string{"foo"}), + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 4, Column: 2, Byte: 40}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.AttrStep{Name: "bar"}, + }, + Type: cty.Number, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 29}, + End: hcl.Pos{Line: 3, Column: 12, Byte: 38}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 29}, + End: hcl.Pos{Line: 3, Column: 8, Byte: 34}, + }, + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.AttrStep{Name: "foo"}, + }, + Type: cty.String, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + End: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + }, + }, + }, + }, + }, + }, + { + "type-aware with invalid attribute name", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Object{ + Attributes: schema.ObjectAttributes{ + "foo": { + Constraint: schema.LiteralType{ + Type: cty.String, + }, + IsOptional: true, + }, + "bar": { + Constraint: schema.LiteralType{ + Type: cty.Number, + }, + IsRequired: true, + }, + }, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `{"attr": { + "fox": "foo", + "bar": 42 +}}`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{ + "foo": cty.String, + "bar": cty.Number, + }, []string{"foo"}), + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 4, Column: 2, Byte: 40}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.AttrStep{Name: "bar"}, + }, + Type: cty.Number, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 29}, + End: hcl.Pos{Line: 3, Column: 12, Byte: 38}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 29}, + End: hcl.Pos{Line: 3, Column: 8, Byte: 34}, + }, + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.AttrStep{Name: "foo"}, + }, + Type: cty.String, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + End: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + }, + }, + }, + }, + }, + }, + { + "type-aware with invalid value type", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Object{ + Attributes: schema.ObjectAttributes{ + "foo": { + Constraint: schema.LiteralType{ + Type: cty.String, + }, + IsOptional: true, + }, + "bar": { + Constraint: schema.LiteralType{ + Type: cty.Number, + }, + IsRequired: true, + }, + }, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `{"attr": { + "foo": 12345, + "bar": 42 +}}`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{ + "foo": cty.String, + "bar": cty.Number, + }, []string{"foo"}), + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 4, Column: 2, Byte: 40}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.AttrStep{Name: "bar"}, + }, + Type: cty.Number, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 29}, + End: hcl.Pos{Line: 3, Column: 12, Byte: 38}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 29}, + End: hcl.Pos{Line: 3, Column: 8, Byte: 34}, + }, + }, + }, + }, + }, + }, + { + "type-unaware", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Object{ + Attributes: schema.ObjectAttributes{ + "foo": { + Constraint: schema.LiteralType{ + Type: cty.String, + }, + IsOptional: true, + }, + "bar": { + Constraint: schema.LiteralType{ + Type: cty.Number, + }, + IsRequired: true, + }, + }, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + ScopeId: lang.ScopeId("test"), + AsReference: true, + }, + }, + }, + `{"attr": { + "foo": "foo", + "bar": 42 +}}`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + ScopeId: lang.ScopeId("test"), + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 4, Column: 2, Byte: 40}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.AttrStep{Name: "bar"}, + }, + ScopeId: lang.ScopeId("test"), + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 29}, + End: hcl.Pos{Line: 3, Column: 12, Byte: 38}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 29}, + End: hcl.Pos{Line: 3, Column: 8, Byte: 34}, + }, + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.AttrStep{Name: "foo"}, + }, + ScopeId: lang.ScopeId("test"), + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 13}, + End: hcl.Pos{Line: 2, Column: 15, Byte: 25}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 13}, + End: hcl.Pos{Line: 2, Column: 8, Byte: 18}, + }, + }, + }, + }, + }, + }, + { + "nested type-unaware", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Object{ + Attributes: schema.ObjectAttributes{ + "foo": { + Constraint: schema.LiteralType{ + Type: cty.String, + }, + IsOptional: true, + }, + "bar": { + Constraint: schema.Object{ + Attributes: schema.ObjectAttributes{ + "baz": { + Constraint: schema.LiteralType{ + Type: cty.String, + }, + IsRequired: true, + }, + }, + }, + IsRequired: true, + }, + }, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `{"attr": { + "foo": "foo", + "bar": { + "baz": "noot" + } +}}`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{ + "foo": cty.String, + "bar": cty.Object(map[string]cty.Type{ + "baz": cty.String, + }), + }, []string{"foo"}), + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 6, Column: 2, Byte: 61}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.AttrStep{Name: "bar"}, + }, + Type: cty.Object(map[string]cty.Type{ + "baz": cty.String, + }), + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 29}, + End: hcl.Pos{Line: 5, Column: 4, Byte: 59}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 29}, + End: hcl.Pos{Line: 3, Column: 8, Byte: 34}, + }, + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.AttrStep{Name: "bar"}, + lang.AttrStep{Name: "baz"}, + }, + Type: cty.String, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 4, Column: 5, Byte: 42}, + End: hcl.Pos{Line: 4, Column: 18, Byte: 55}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 4, Column: 5, Byte: 42}, + End: hcl.Pos{Line: 4, Column: 10, Byte: 47}, + }, + }, + }, + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.AttrStep{Name: "foo"}, + }, + Type: cty.String, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 13}, + End: hcl.Pos{Line: 2, Column: 15, Byte: 25}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 13}, + End: hcl.Pos{Line: 2, Column: 8, Byte: 18}, + }, + }, + }, + }, + }, + }, + } + for i, tc := range testCases { + t.Run(fmt.Sprintf("%d-%s", i, tc.testName), func(t *testing.T) { + bodySchema := &schema.BodySchema{ + Attributes: tc.attrSchema, + } + + f, diags := json.ParseWithStartPos([]byte(tc.cfg), "test.hcl.json", hcl.InitialPos) + if len(diags) > 0 { + t.Error(diags) + } + d := testPathDecoder(t, &PathContext{ + Schema: bodySchema, + Files: map[string]*hcl.File{ + "test.hcl.json": f, + }, + }) + + targets, err := d.CollectReferenceTargets() + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(tc.expectedRefTargets, targets, ctydebug.CmpOptions); diff != "" { + t.Fatalf("unexpected targets: %s", diff) + } + }) + } +} diff --git a/decoder/expr_one_of.go b/decoder/expr_one_of.go index 8c95b07e..ad4be29c 100644 --- a/decoder/expr_one_of.go +++ b/decoder/expr_one_of.go @@ -3,6 +3,7 @@ package decoder import ( "github.com/hashicorp/hcl-lang/schema" "github.com/hashicorp/hcl/v2" + "github.com/zclconf/go-cty/cty" ) type OneOf struct { @@ -10,3 +11,32 @@ type OneOf struct { cons schema.OneOf pathCtx *PathContext } + +func (oo OneOf) InferType() (cty.Type, bool) { + consType, ok := oo.cons.ConstraintType() + if !ok { + return consType, false + } + + if consType == cty.DynamicPseudoType && !isEmptyExpression(oo.expr) { + for _, cons := range oo.cons { + c, ok := cons.(CanInferTypeExpression) + if !ok { + continue + } + typ, ok := c.InferType() + if !ok { + continue + } + + // Picking first type-aware constraint may not always be + // appropriate since we cannot match it against configuration, + // but it is mostly a pragmatic choice to mimic existing behaviours + // based on common schema, such as OneOf{Reference{}, LiteralType{}}. + // TODO: Revisit when AnyExpression{} is implemented & rolled out + return typ, true + } + } + + return consType, true +} diff --git a/decoder/expr_one_of_ref_targets_test.go b/decoder/expr_one_of_ref_targets_test.go index 7c97c077..c0a999a9 100644 --- a/decoder/expr_one_of_ref_targets_test.go +++ b/decoder/expr_one_of_ref_targets_test.go @@ -1,7 +1,388 @@ package decoder -import "testing" +import ( + "fmt" + "testing" -func TestCollectRefTargets_exprOneOf(t *testing.T) { - // TODO! test when reference expr is available + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/hcl-lang/lang" + "github.com/hashicorp/hcl-lang/reference" + "github.com/hashicorp/hcl-lang/schema" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/hashicorp/hcl/v2/json" + "github.com/zclconf/go-cty-debug/ctydebug" + "github.com/zclconf/go-cty/cty" +) + +func TestCollectRefTargets_exprOneOf_hcl(t *testing.T) { + testCases := []struct { + testName string + attrSchema map[string]*schema.AttributeSchema + cfg string + expectedRefTargets reference.Targets + }{ + { + "constraint mismatch", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.OneOf{ + schema.LiteralType{Type: cty.Number}, + schema.LiteralType{Type: cty.String}, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `attr = true`, + reference.Targets{}, + }, + { + "first constraint match", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.OneOf{ + schema.LiteralType{Type: cty.Bool}, + schema.LiteralType{Type: cty.String}, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `attr = true`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 12, Byte: 11}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + Type: cty.Bool, + }, + }, + }, + { + "second constraint match", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.OneOf{ + schema.LiteralType{Type: cty.String}, + schema.LiteralType{Type: cty.Bool}, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `attr = true`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 12, Byte: 11}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + Type: cty.Bool, + }, + }, + }, + { + "double constraint match", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.OneOf{ + schema.List{Elem: schema.LiteralType{Type: cty.String}}, + schema.Set{Elem: schema.LiteralType{Type: cty.String}}, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `attr = ["foo"]`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 15, Byte: 14}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + Type: cty.List(cty.String), + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(0)}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 9, Byte: 8}, + End: hcl.Pos{Line: 1, Column: 14, Byte: 13}, + }, + Type: cty.String, + }, + }, + }, + }, + }, + } + for i, tc := range testCases { + t.Run(fmt.Sprintf("%d-%s", i, tc.testName), func(t *testing.T) { + bodySchema := &schema.BodySchema{ + Attributes: tc.attrSchema, + } + + f, diags := hclsyntax.ParseConfig([]byte(tc.cfg), "test.hcl", hcl.InitialPos) + if len(diags) > 0 { + t.Error(diags) + } + d := testPathDecoder(t, &PathContext{ + Schema: bodySchema, + Files: map[string]*hcl.File{ + "test.hcl": f, + }, + }) + + targets, err := d.CollectReferenceTargets() + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(tc.expectedRefTargets, targets, ctydebug.CmpOptions); diff != "" { + t.Fatalf("unexpected targets: %s", diff) + } + }) + } +} + +func TestCollectRefTargets_exprOneOf_json(t *testing.T) { + testCases := []struct { + testName string + attrSchema map[string]*schema.AttributeSchema + cfg string + expectedRefTargets reference.Targets + }{ + { + "constraint mismatch", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.OneOf{ + schema.LiteralType{Type: cty.Number}, + schema.LiteralType{Type: cty.String}, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `{"attr": true}`, + reference.Targets{}, + }, + { + "first constraint match", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.OneOf{ + schema.LiteralType{Type: cty.Bool}, + schema.LiteralType{Type: cty.String}, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `{"attr": true}`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 14, Byte: 13}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + Type: cty.Bool, + }, + }, + }, + { + "second constraint match", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.OneOf{ + schema.LiteralType{Type: cty.String}, + schema.LiteralType{Type: cty.Bool}, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `{"attr": true}`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 14, Byte: 13}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + Type: cty.Bool, + }, + }, + }, + { + "double constraint match", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.OneOf{ + schema.List{ + Elem: schema.LiteralType{ + Type: cty.String, + }, + }, + schema.Set{ + Elem: schema.LiteralType{ + Type: cty.String, + }, + }, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `{"attr": ["foo"]}`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 17, Byte: 16}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + Type: cty.List(cty.String), + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(0)}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 11, Byte: 10}, + End: hcl.Pos{Line: 1, Column: 16, Byte: 15}, + }, + Type: cty.String, + }, + }, + }, + }, + }, + } + for i, tc := range testCases { + t.Run(fmt.Sprintf("%d-%s", i, tc.testName), func(t *testing.T) { + bodySchema := &schema.BodySchema{ + Attributes: tc.attrSchema, + } + + f, diags := json.ParseWithStartPos([]byte(tc.cfg), "test.hcl.json", hcl.InitialPos) + if len(diags) > 0 { + t.Error(diags) + } + d := testPathDecoder(t, &PathContext{ + Schema: bodySchema, + Files: map[string]*hcl.File{ + "test.hcl.json": f, + }, + }) + + targets, err := d.CollectReferenceTargets() + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(tc.expectedRefTargets, targets, ctydebug.CmpOptions); diff != "" { + t.Fatalf("unexpected targets: %s", diff) + } + }) + } } diff --git a/decoder/expr_reference_ref_targets.go b/decoder/expr_reference_ref_targets.go index 2fdcb559..c48e3cfc 100644 --- a/decoder/expr_reference_ref_targets.go +++ b/decoder/expr_reference_ref_targets.go @@ -8,6 +8,7 @@ import ( "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" "github.com/hashicorp/hcl/v2/json" + "github.com/zclconf/go-cty/cty" ) func (ref Reference) ReferenceTargets(ctx context.Context, _ *TargetContext) reference.Targets { @@ -38,41 +39,73 @@ func (ref Reference) ReferenceTargets(ctx context.Context, _ *TargetContext) ref // guess whether the expression has exactly a single traversal vars := ref.expr.Variables() - if len(vars) != 1 { - return reference.Targets{} + if len(vars) == 1 { + tRange := vars[0].SourceRange() + expectedExprRange := hcl.Range{ + Filename: tRange.Filename, + Start: hcl.Pos{ + Line: tRange.Start.Line, + // account for "${ + Column: tRange.Start.Column - 3, + Byte: tRange.Start.Byte - 3, + }, + End: hcl.Pos{ + Line: tRange.End.Line, + // account for }" + Column: tRange.End.Column + 2, + Byte: tRange.End.Byte + 2, + }, + } + + if rangesEqual(expectedExprRange, ref.expr.Range()) { + addr, err := lang.TraversalToAddress(vars[0]) + if err != nil { + return reference.Targets{} + } + + return reference.Targets{ + reference.Target{ + Addr: addr, + ScopeId: ref.cons.Address.ScopeId, + RangePtr: vars[0].SourceRange().Ptr(), + Name: ref.cons.Name, + }, + } + } } - tRange := vars[0].SourceRange() - expectedExprRange := hcl.Range{ - Filename: tRange.Filename, - Start: hcl.Pos{ - Line: tRange.Start.Line, - // account for "${ - Column: tRange.Start.Column - 3, - Byte: tRange.Start.Byte - 3, - }, - End: hcl.Pos{ - Line: tRange.End.Line, - // account for }" - Column: tRange.End.Column + 2, - Byte: tRange.End.Byte + 2, - }, + // Account for "legacy" string syntax which is still + // in use by Terraform to date in this context. + val, diags := ref.expr.Value(&hcl.EvalContext{}) + if diags.HasErrors() { + return reference.Targets{} + } + if val.Type() != cty.String { + return reference.Targets{} + } + startPos := hcl.Pos{ + Line: ref.expr.Range().Start.Line, + // Account for the leading double quote + Column: ref.expr.Range().Start.Column + 1, + Byte: ref.expr.Range().Start.Byte + 1, } - if rangesEqual(expectedExprRange, ref.expr.Range()) { - addr, err := lang.TraversalToAddress(vars[0]) - if err != nil { - return reference.Targets{} - } + traversal, diags := hclsyntax.ParseTraversalAbs([]byte(val.AsString()), ref.expr.Range().Filename, startPos) + if diags.HasErrors() { + return reference.Targets{} + } + addr, err := lang.TraversalToAddress(traversal) + if err != nil { + return reference.Targets{} + } - return reference.Targets{ - reference.Target{ - Addr: addr, - ScopeId: ref.cons.Address.ScopeId, - RangePtr: vars[0].SourceRange().Ptr(), - Name: ref.cons.Name, - }, - } + return reference.Targets{ + reference.Target{ + Addr: addr, + ScopeId: ref.cons.Address.ScopeId, + RangePtr: traversal.SourceRange().Ptr(), + Name: ref.cons.Name, + }, } } diff --git a/decoder/expr_reference_ref_targets_test.go b/decoder/expr_reference_ref_targets_test.go index adf56e2a..a04adb1a 100644 --- a/decoder/expr_reference_ref_targets_test.go +++ b/decoder/expr_reference_ref_targets_test.go @@ -159,7 +159,7 @@ func TestCollectRefTargets_exprReference_json(t *testing.T) { IsOptional: true, }, }, - `{"attr": "foo"}`, + `{"attr": 422}`, reference.Targets{}, }, { diff --git a/decoder/expr_set_ref_targets.go b/decoder/expr_set_ref_targets.go index fe2e13a4..86cad283 100644 --- a/decoder/expr_set_ref_targets.go +++ b/decoder/expr_set_ref_targets.go @@ -4,23 +4,84 @@ import ( "context" "github.com/hashicorp/hcl-lang/reference" - "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/hashicorp/hcl-lang/schema" + "github.com/hashicorp/hcl/v2" + "github.com/zclconf/go-cty/cty" ) func (set Set) ReferenceTargets(ctx context.Context, targetCtx *TargetContext) reference.Targets { - eType, ok := set.expr.(*hclsyntax.TupleConsExpr) - if !ok { + elems, diags := hcl.ExprList(set.expr) + if diags.HasErrors() { return reference.Targets{} } - if len(eType.Exprs) == 0 || set.cons.Elem == nil { + if set.cons.Elem == nil { return reference.Targets{} } + elemTargets := make(reference.Targets, 0) + + for _, elemExpr := range elems { + expr := newExpression(set.pathCtx, elemExpr, set.cons.Elem) + if e, ok := expr.(ReferenceTargetsExpression); ok { + if targetCtx == nil { + // collect any targets inside the expression + // as set elements aren't addressable by themselves + elemTargets = append(elemTargets, e.ReferenceTargets(ctx, nil)...) + continue + } + } + } + targets := make(reference.Targets, 0) - // TODO: collect parent target for the whole set - // See https://github.com/hashicorp/hcl-lang/issues/228 + if targetCtx != nil { + // collect target for the whole set + + var rangePtr *hcl.Range + if targetCtx.ParentRangePtr != nil { + rangePtr = targetCtx.ParentRangePtr + } else { + rangePtr = set.expr.Range().Ptr() + } + + // type-aware + elemCons, ok := set.cons.Elem.(schema.TypeAwareConstraint) + if targetCtx.AsExprType && ok { + elemType, ok := elemCons.ConstraintType() + if ok { + targets = append(targets, reference.Target{ + Addr: targetCtx.ParentAddress, + Name: targetCtx.FriendlyName, + Type: cty.Set(elemType), + ScopeId: targetCtx.ScopeId, + RangePtr: rangePtr, + DefRangePtr: targetCtx.ParentDefRangePtr, + NestedTargets: elemTargets, + LocalAddr: targetCtx.ParentLocalAddress, + TargetableFromRangePtr: targetCtx.TargetableFromRangePtr, + }) + } + } + + // type-unaware + if targetCtx.AsReference { + targets = append(targets, reference.Target{ + Addr: targetCtx.ParentAddress, + Name: targetCtx.FriendlyName, + ScopeId: targetCtx.ScopeId, + RangePtr: rangePtr, + DefRangePtr: targetCtx.ParentDefRangePtr, + NestedTargets: elemTargets, + LocalAddr: targetCtx.ParentLocalAddress, + TargetableFromRangePtr: targetCtx.TargetableFromRangePtr, + }) + } + } else { + // treat element targets as 1st class ones + // if the list itself isn't targetable + targets = elemTargets + } return targets } diff --git a/decoder/expr_set_ref_targets_test.go b/decoder/expr_set_ref_targets_test.go new file mode 100644 index 00000000..f7ab4b50 --- /dev/null +++ b/decoder/expr_set_ref_targets_test.go @@ -0,0 +1,554 @@ +package decoder + +import ( + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/hcl-lang/lang" + "github.com/hashicorp/hcl-lang/reference" + "github.com/hashicorp/hcl-lang/schema" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/hashicorp/hcl/v2/json" + "github.com/zclconf/go-cty-debug/ctydebug" + "github.com/zclconf/go-cty/cty" +) + +func TestCollectRefTargets_exprSet_hcl(t *testing.T) { + testCases := []struct { + testName string + attrSchema map[string]*schema.AttributeSchema + cfg string + expectedRefTargets reference.Targets + }{ + { + "constraint mismatch", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Set{ + Elem: schema.LiteralType{Type: cty.Bool}, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `attr = true`, + reference.Targets{}, + }, + { + "set of keyword", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Set{ + Elem: schema.Keyword{Keyword: "foo"}, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `attr = [foo]`, + reference.Targets{}, + }, + { + "set of addressable reference", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Set{ + Elem: schema.Reference{ + Address: &schema.ReferenceAddrSchema{ + ScopeId: lang.ScopeId("test"), + }, + }, + }, + IsOptional: true, + }, + }, + `attr = [foo]`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "foo"}, + }, + ScopeId: lang.ScopeId("test"), + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 9, Byte: 8}, + End: hcl.Pos{Line: 1, Column: 12, Byte: 11}, + }, + }, + }, + }, + { + "empty type-aware", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Set{ + Elem: schema.LiteralType{ + Type: cty.String, + }, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `attr = []`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + Type: cty.Set(cty.String), + NestedTargets: reference.Targets{}, + }, + }, + }, + { + "type-aware with invalid element", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Set{ + Elem: schema.LiteralType{ + Type: cty.String, + }, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `attr = ["one", foo, "two"]`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 27, Byte: 26}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + Type: cty.Set(cty.String), + NestedTargets: reference.Targets{}, + }, + }, + }, + { + "type-unaware", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Set{ + Elem: schema.LiteralType{ + Type: cty.String, + }, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + ScopeId: lang.ScopeId("test"), + AsReference: true, + }, + }, + }, + `attr = ["one", "two"]`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 22, Byte: 21}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + ScopeId: lang.ScopeId("test"), + NestedTargets: reference.Targets{}, + }, + }, + }, + { + "type-aware nested", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Set{ + Elem: schema.Set{ + Elem: schema.LiteralType{ + Type: cty.String, + }, + }, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `attr = [ + ["one"], + ["two"], +] +`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 4, Column: 2, Byte: 32}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + Type: cty.Set(cty.Set(cty.String)), + NestedTargets: reference.Targets{}, + }, + }, + }, + } + for i, tc := range testCases { + t.Run(fmt.Sprintf("%d-%s", i, tc.testName), func(t *testing.T) { + bodySchema := &schema.BodySchema{ + Attributes: tc.attrSchema, + } + + f, diags := hclsyntax.ParseConfig([]byte(tc.cfg), "test.hcl", hcl.InitialPos) + if len(diags) > 0 { + t.Error(diags) + } + d := testPathDecoder(t, &PathContext{ + Schema: bodySchema, + Files: map[string]*hcl.File{ + "test.hcl": f, + }, + }) + + targets, err := d.CollectReferenceTargets() + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(tc.expectedRefTargets, targets, ctydebug.CmpOptions); diff != "" { + t.Fatalf("unexpected targets: %s", diff) + } + }) + } +} + +func TestCollectRefTargets_exprSet_json(t *testing.T) { + testCases := []struct { + testName string + attrSchema map[string]*schema.AttributeSchema + cfg string + expectedRefTargets reference.Targets + }{ + { + "constraint mismatch", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Set{ + Elem: schema.LiteralType{Type: cty.Bool}, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `{"attr": true}`, + reference.Targets{}, + }, + { + "set of keyword", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Set{ + Elem: schema.Keyword{Keyword: "foo"}, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `{"attr": ["foo"]}`, + reference.Targets{}, + }, + { + "set of addressable reference", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Set{ + Elem: schema.Reference{ + Address: &schema.ReferenceAddrSchema{ + ScopeId: lang.ScopeId("test"), + }, + }, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + }, + }, + }, + `{"attr": ["foo"]}`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "foo"}, + }, + ScopeId: lang.ScopeId("test"), + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 12, Byte: 11}, + End: hcl.Pos{Line: 1, Column: 15, Byte: 14}, + }, + }, + }, + }, + { + "empty type-aware", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Set{ + Elem: schema.LiteralType{ + Type: cty.String, + }, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `{"attr": []}`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 12, Byte: 11}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + Type: cty.Set(cty.String), + NestedTargets: reference.Targets{}, + }, + }, + }, + { + "type-aware with invalid element", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Set{ + Elem: schema.LiteralType{ + Type: cty.String, + }, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `{"attr": ["one", 422, "two"]}`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 29, Byte: 28}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + Type: cty.Set(cty.String), + NestedTargets: reference.Targets{}, + }, + }, + }, + { + "type-unaware", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Set{ + Elem: schema.LiteralType{ + Type: cty.String, + }, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + ScopeId: lang.ScopeId("test"), + AsReference: true, + }, + }, + }, + `{"attr": ["one", "two"]}`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 24, Byte: 23}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + ScopeId: lang.ScopeId("test"), + NestedTargets: reference.Targets{}, + }, + }, + }, + { + "type-aware nested", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Set{ + Elem: schema.Set{ + Elem: schema.LiteralType{ + Type: cty.String, + }, + }, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `{"attr": [ + ["one"], + ["two"] +]}`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 4, Column: 2, Byte: 33}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + Type: cty.Set(cty.Set(cty.String)), + NestedTargets: reference.Targets{}, + }, + }, + }, + } + for i, tc := range testCases { + t.Run(fmt.Sprintf("%d-%s", i, tc.testName), func(t *testing.T) { + bodySchema := &schema.BodySchema{ + Attributes: tc.attrSchema, + } + + f, diags := json.ParseWithStartPos([]byte(tc.cfg), "test.hcl.json", hcl.InitialPos) + if len(diags) > 0 { + t.Error(diags) + } + d := testPathDecoder(t, &PathContext{ + Schema: bodySchema, + Files: map[string]*hcl.File{ + "test.hcl.json": f, + }, + }) + + targets, err := d.CollectReferenceTargets() + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(tc.expectedRefTargets, targets, ctydebug.CmpOptions); diff != "" { + t.Fatalf("unexpected targets: %s", diff) + } + }) + } +} diff --git a/decoder/expr_tuple_ref_targets.go b/decoder/expr_tuple_ref_targets.go index d02bb85c..4e05e8cb 100644 --- a/decoder/expr_tuple_ref_targets.go +++ b/decoder/expr_tuple_ref_targets.go @@ -5,32 +5,36 @@ import ( "github.com/hashicorp/hcl-lang/lang" "github.com/hashicorp/hcl-lang/reference" - "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/hashicorp/hcl/v2" "github.com/zclconf/go-cty/cty" ) func (tuple Tuple) ReferenceTargets(ctx context.Context, targetCtx *TargetContext) reference.Targets { - eType, ok := tuple.expr.(*hclsyntax.TupleConsExpr) - if !ok { + elems, diags := hcl.ExprList(tuple.expr) + if diags.HasErrors() { return reference.Targets{} } - if len(eType.Exprs) == 0 || len(tuple.cons.Elems) == 0 { + if len(tuple.cons.Elems) == 0 { return reference.Targets{} } - targets := make(reference.Targets, 0) - - // TODO: collect parent target for the whole tuple - // See https://github.com/hashicorp/hcl-lang/issues/228 + elemTargets := make(reference.Targets, 0) - for i, elemExpr := range eType.Exprs { + for i, elemExpr := range elems { if i+1 > len(tuple.cons.Elems) { break } expr := newExpression(tuple.pathCtx, elemExpr, tuple.cons.Elems[i]) if e, ok := expr.(ReferenceTargetsExpression); ok { + if targetCtx == nil { + // collect any targets inside the expression + // if attribute itself isn't targetable + elemTargets = append(elemTargets, e.ReferenceTargets(ctx, nil)...) + continue + } + elemCtx := targetCtx.Copy() elemCtx.ParentAddress = append(elemCtx.ParentAddress, lang.IndexStep{ Key: cty.NumberIntVal(int64(i)), @@ -40,8 +44,57 @@ func (tuple Tuple) ReferenceTargets(ctx context.Context, targetCtx *TargetContex Key: cty.NumberIntVal(int64(i)), }) } - targets = append(targets, e.ReferenceTargets(ctx, elemCtx)...) + elemTargets = append(elemTargets, e.ReferenceTargets(ctx, elemCtx)...) + } + } + + targets := make(reference.Targets, 0) + + if targetCtx != nil { + // collect target for the whole tuple + + var rangePtr *hcl.Range + if targetCtx.ParentRangePtr != nil { + rangePtr = targetCtx.ParentRangePtr + } else { + rangePtr = tuple.expr.Range().Ptr() + } + + // type-aware + elemType, ok := tuple.cons.ConstraintType() + if targetCtx.AsExprType && ok { + if ok { + targets = append(targets, reference.Target{ + Addr: targetCtx.ParentAddress, + Name: targetCtx.FriendlyName, + Type: elemType, + ScopeId: targetCtx.ScopeId, + RangePtr: rangePtr, + DefRangePtr: targetCtx.ParentDefRangePtr, + NestedTargets: elemTargets, + LocalAddr: targetCtx.ParentLocalAddress, + TargetableFromRangePtr: targetCtx.TargetableFromRangePtr, + }) + } + } + + // type-unaware + if targetCtx.AsReference { + targets = append(targets, reference.Target{ + Addr: targetCtx.ParentAddress, + Name: targetCtx.FriendlyName, + ScopeId: targetCtx.ScopeId, + RangePtr: rangePtr, + DefRangePtr: targetCtx.ParentDefRangePtr, + NestedTargets: elemTargets, + LocalAddr: targetCtx.ParentLocalAddress, + TargetableFromRangePtr: targetCtx.TargetableFromRangePtr, + }) } + } else { + // treat element targets as 1st class ones + // if the tuple itself isn't targetable + targets = elemTargets } return targets diff --git a/decoder/expr_tuple_ref_targets_test.go b/decoder/expr_tuple_ref_targets_test.go new file mode 100644 index 00000000..9e7f0cd2 --- /dev/null +++ b/decoder/expr_tuple_ref_targets_test.go @@ -0,0 +1,903 @@ +package decoder + +import ( + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/hcl-lang/lang" + "github.com/hashicorp/hcl-lang/reference" + "github.com/hashicorp/hcl-lang/schema" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/hashicorp/hcl/v2/json" + "github.com/zclconf/go-cty-debug/ctydebug" + "github.com/zclconf/go-cty/cty" +) + +func TestCollectRefTargets_exprTuple_hcl(t *testing.T) { + testCases := []struct { + testName string + attrSchema map[string]*schema.AttributeSchema + cfg string + expectedRefTargets reference.Targets + }{ + { + "constraint mismatch", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Tuple{ + Elems: []schema.Constraint{ + schema.LiteralType{Type: cty.Bool}, + }, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `attr = true`, + reference.Targets{}, + }, + { + "tuple of keyword", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Tuple{ + Elems: []schema.Constraint{ + schema.Keyword{Keyword: "foo"}, + }, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `attr = [foo]`, + reference.Targets{}, + }, + { + "tuple of addressable reference", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Tuple{ + Elems: []schema.Constraint{ + schema.Reference{ + Address: &schema.ReferenceAddrSchema{ + ScopeId: lang.ScopeId("test"), + }, + }, + }, + }, + IsOptional: true, + }, + }, + `attr = [foo]`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "foo"}, + }, + ScopeId: lang.ScopeId("test"), + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 9, Byte: 8}, + End: hcl.Pos{Line: 1, Column: 12, Byte: 11}, + }, + }, + }, + }, + { + "empty type-aware", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Tuple{ + Elems: []schema.Constraint{ + schema.LiteralType{ + Type: cty.String, + }, + }, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `attr = []`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + Type: cty.Tuple([]cty.Type{cty.String}), + NestedTargets: reference.Targets{}, + }, + }, + }, + { + "type-aware with invalid element", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Tuple{ + Elems: []schema.Constraint{ + schema.LiteralType{ + Type: cty.String, + }, + schema.LiteralType{ + Type: cty.String, + }, + schema.LiteralType{ + Type: cty.Number, + }, + }, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `attr = ["one", foo, 42224]`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 27, Byte: 26}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + Type: cty.Tuple([]cty.Type{cty.String, cty.String, cty.Number}), + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(0)}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 9, Byte: 8}, + End: hcl.Pos{Line: 1, Column: 14, Byte: 13}, + }, + Type: cty.String, + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(2)}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 21, Byte: 20}, + End: hcl.Pos{Line: 1, Column: 26, Byte: 25}, + }, + Type: cty.Number, + }, + }, + }, + }, + }, + { + "type-aware with extra element", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Tuple{ + Elems: []schema.Constraint{ + schema.LiteralType{ + Type: cty.String, + }, + schema.LiteralType{ + Type: cty.Number, + }, + }, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `attr = ["one", 422, "two"]`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 27, Byte: 26}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + Type: cty.Tuple([]cty.Type{cty.String, cty.Number}), + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(0)}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 9, Byte: 8}, + End: hcl.Pos{Line: 1, Column: 14, Byte: 13}, + }, + Type: cty.String, + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(1)}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 16, Byte: 15}, + End: hcl.Pos{Line: 1, Column: 19, Byte: 18}, + }, + Type: cty.Number, + }, + }, + }, + }, + }, + { + "type-unaware", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Tuple{ + Elems: []schema.Constraint{ + schema.LiteralType{ + Type: cty.String, + }, + schema.LiteralType{ + Type: cty.String, + }, + }, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + ScopeId: lang.ScopeId("test"), + AsReference: true, + }, + }, + }, + `attr = ["one", "two"]`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 22, Byte: 21}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + ScopeId: lang.ScopeId("test"), + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(0)}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 9, Byte: 8}, + End: hcl.Pos{Line: 1, Column: 14, Byte: 13}, + }, + ScopeId: lang.ScopeId("test"), + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(1)}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 16, Byte: 15}, + End: hcl.Pos{Line: 1, Column: 21, Byte: 20}, + }, + ScopeId: lang.ScopeId("test"), + }, + }, + }, + }, + }, + { + "type-aware nested", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Tuple{ + Elems: []schema.Constraint{ + schema.Tuple{ + Elems: []schema.Constraint{ + schema.LiteralType{ + Type: cty.String, + }, + }, + }, + schema.Tuple{ + Elems: []schema.Constraint{ + schema.LiteralType{ + Type: cty.String, + }, + }, + }, + }, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `attr = [ + ["one"], + ["two"], +] +`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 4, Column: 2, Byte: 32}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + Type: cty.Tuple([]cty.Type{ + cty.Tuple([]cty.Type{cty.String}), + cty.Tuple([]cty.Type{cty.String}), + }), + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(0)}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 11}, + End: hcl.Pos{Line: 2, Column: 10, Byte: 18}, + }, + Type: cty.Tuple([]cty.Type{cty.String}), + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(0)}, + lang.IndexStep{Key: cty.NumberIntVal(0)}, + }, + Type: cty.String, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 2, Column: 4, Byte: 12}, + End: hcl.Pos{Line: 2, Column: 9, Byte: 17}, + }, + }, + }, + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(1)}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 22}, + End: hcl.Pos{Line: 3, Column: 10, Byte: 29}, + }, + Type: cty.Tuple([]cty.Type{cty.String}), + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(1)}, + lang.IndexStep{Key: cty.NumberIntVal(0)}, + }, + Type: cty.String, + RangePtr: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 3, Column: 4, Byte: 23}, + End: hcl.Pos{Line: 3, Column: 9, Byte: 28}, + }, + }, + }, + }, + }, + }, + }, + }, + } + for i, tc := range testCases { + t.Run(fmt.Sprintf("%d-%s", i, tc.testName), func(t *testing.T) { + bodySchema := &schema.BodySchema{ + Attributes: tc.attrSchema, + } + + f, diags := hclsyntax.ParseConfig([]byte(tc.cfg), "test.hcl", hcl.InitialPos) + if len(diags) > 0 { + t.Error(diags) + } + d := testPathDecoder(t, &PathContext{ + Schema: bodySchema, + Files: map[string]*hcl.File{ + "test.hcl": f, + }, + }) + + targets, err := d.CollectReferenceTargets() + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(tc.expectedRefTargets, targets, ctydebug.CmpOptions); diff != "" { + t.Fatalf("unexpected targets: %s", diff) + } + }) + } +} + +func TestCollectRefTargets_exprTuple_json(t *testing.T) { + testCases := []struct { + testName string + attrSchema map[string]*schema.AttributeSchema + cfg string + expectedRefTargets reference.Targets + }{ + { + "constraint mismatch", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Tuple{ + Elems: []schema.Constraint{ + schema.LiteralType{Type: cty.Bool}, + }, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `{"attr": true}`, + reference.Targets{}, + }, + { + "tuple of keyword", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Tuple{ + Elems: []schema.Constraint{ + schema.Keyword{Keyword: "foo"}, + }, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `{"attr": ["foo"]}`, + reference.Targets{}, + }, + { + "tuple of addressable reference", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Tuple{ + Elems: []schema.Constraint{ + schema.Reference{ + Address: &schema.ReferenceAddrSchema{ + ScopeId: lang.ScopeId("test"), + }, + }, + }, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + }, + }, + }, + `{"attr": ["foo"]}`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "foo"}, + }, + ScopeId: lang.ScopeId("test"), + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 12, Byte: 11}, + End: hcl.Pos{Line: 1, Column: 15, Byte: 14}, + }, + }, + }, + }, + { + "empty type-aware", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Tuple{ + Elems: []schema.Constraint{ + schema.LiteralType{ + Type: cty.String, + }, + }, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `{"attr": []}`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 12, Byte: 11}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + Type: cty.Tuple([]cty.Type{cty.String}), + NestedTargets: reference.Targets{}, + }, + }, + }, + { + "type-aware with invalid element", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Tuple{ + Elems: []schema.Constraint{ + schema.LiteralType{ + Type: cty.String, + }, + schema.LiteralType{ + Type: cty.String, + }, + schema.LiteralType{ + Type: cty.Number, + }, + }, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `{"attr": ["one", 422, 42223]}`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 29, Byte: 28}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + Type: cty.Tuple([]cty.Type{cty.String, cty.String, cty.Number}), + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(0)}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 11, Byte: 10}, + End: hcl.Pos{Line: 1, Column: 16, Byte: 15}, + }, + Type: cty.String, + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(2)}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 23, Byte: 22}, + End: hcl.Pos{Line: 1, Column: 28, Byte: 27}, + }, + Type: cty.Number, + }, + }, + }, + }, + }, + { + "type-unaware", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Tuple{ + Elems: []schema.Constraint{ + schema.LiteralType{ + Type: cty.String, + }, + schema.LiteralType{ + Type: cty.String, + }, + }, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + ScopeId: lang.ScopeId("test"), + AsReference: true, + }, + }, + }, + `{"attr": ["one", "two"]}`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 24, Byte: 23}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + ScopeId: lang.ScopeId("test"), + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(0)}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 11, Byte: 10}, + End: hcl.Pos{Line: 1, Column: 16, Byte: 15}, + }, + ScopeId: lang.ScopeId("test"), + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(1)}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 18, Byte: 17}, + End: hcl.Pos{Line: 1, Column: 23, Byte: 22}, + }, + ScopeId: lang.ScopeId("test"), + }, + }, + }, + }, + }, + { + "type-aware nested", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Tuple{ + Elems: []schema.Constraint{ + schema.Tuple{ + Elems: []schema.Constraint{ + schema.LiteralType{ + Type: cty.String, + }, + }, + }, + schema.Tuple{ + Elems: []schema.Constraint{ + schema.LiteralType{ + Type: cty.String, + }, + }, + }, + }, + }, + IsOptional: true, + Address: &schema.AttributeAddrSchema{ + Steps: schema.Address{ + schema.AttrNameStep{}, + }, + AsExprType: true, + }, + }, + }, + `{"attr": [ + ["one"], + ["two"] +]}`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 4, Column: 2, Byte: 33}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 2, Byte: 1}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + Type: cty.Tuple([]cty.Type{ + cty.Tuple([]cty.Type{cty.String}), + cty.Tuple([]cty.Type{cty.String}), + }), + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(0)}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 13}, + End: hcl.Pos{Line: 2, Column: 10, Byte: 20}, + }, + Type: cty.Tuple([]cty.Type{cty.String}), + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(0)}, + lang.IndexStep{Key: cty.NumberIntVal(0)}, + }, + Type: cty.String, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 2, Column: 4, Byte: 14}, + End: hcl.Pos{Line: 2, Column: 9, Byte: 19}, + }, + }, + }, + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(1)}, + }, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 24}, + End: hcl.Pos{Line: 3, Column: 10, Byte: 31}, + }, + Type: cty.Tuple([]cty.Type{cty.String}), + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "attr"}, + lang.IndexStep{Key: cty.NumberIntVal(1)}, + lang.IndexStep{Key: cty.NumberIntVal(0)}, + }, + Type: cty.String, + RangePtr: &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 3, Column: 4, Byte: 25}, + End: hcl.Pos{Line: 3, Column: 9, Byte: 30}, + }, + }, + }, + }, + }, + }, + }, + }, + } + for i, tc := range testCases { + t.Run(fmt.Sprintf("%d-%s", i, tc.testName), func(t *testing.T) { + bodySchema := &schema.BodySchema{ + Attributes: tc.attrSchema, + } + + f, diags := json.ParseWithStartPos([]byte(tc.cfg), "test.hcl.json", hcl.InitialPos) + if len(diags) > 0 { + t.Error(diags) + } + d := testPathDecoder(t, &PathContext{ + Schema: bodySchema, + Files: map[string]*hcl.File{ + "test.hcl.json": f, + }, + }) + + targets, err := d.CollectReferenceTargets() + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(tc.expectedRefTargets, targets, ctydebug.CmpOptions); diff != "" { + t.Fatalf("unexpected targets: %s", diff) + } + }) + } +} diff --git a/decoder/expression.go b/decoder/expression.go index 2c5e252e..e720026e 100644 --- a/decoder/expression.go +++ b/decoder/expression.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/hcl-lang/schema" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/hashicorp/hcl/v2/json" "github.com/zclconf/go-cty/cty" ) @@ -28,6 +29,10 @@ type ReferenceTargetsExpression interface { ReferenceTargets(ctx context.Context, targetCtx *TargetContext) reference.Targets } +type CanInferTypeExpression interface { + InferType() (cty.Type, bool) +} + // TargetContext describes context for collecting reference targets type TargetContext struct { // FriendlyName is (optional) human-readable name of the expression @@ -66,6 +71,14 @@ type TargetContext struct { // TargetableFromRangePtr defines where the target is locally targetable // from via the ParentLocalAddress. TargetableFromRangePtr *hcl.Range + + // ParentRangePtr represents the range of the parent target being collected + // e.g. whole object/map item + ParentRangePtr *hcl.Range + + // ParentDefRangePtr represents the range of the parent target's definition + // e.g. object attribute name or map key + ParentDefRangePtr *hcl.Range } func (tctx *TargetContext) Copy() *TargetContext { @@ -254,6 +267,18 @@ func isObjectItemTerminatingRune(r rune) bool { // It does *not* account for interpolation inside the key, // such as { (var.key_name) = "foo" }. func rawObjectKey(expr hcl.Expression) (string, *hcl.Range, bool) { + if json.IsJSONExpression(expr) { + val, diags := expr.Value(&hcl.EvalContext{}) + if diags.HasErrors() { + return "", nil, false + } + if val.Type() != cty.String { + return "", nil, false + } + + return val.AsString(), expr.Range().Ptr(), true + } + // regardless of what expression it is always wrapped keyExpr, ok := expr.(*hclsyntax.ObjectConsKeyExpr) if !ok { diff --git a/decoder/expression_test.go b/decoder/expression_test.go index 02cf6c80..901aa94b 100644 --- a/decoder/expression_test.go +++ b/decoder/expression_test.go @@ -9,6 +9,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/hashicorp/hcl/v2/json" ) var ( @@ -118,6 +119,26 @@ func TestRawObjectKey(t *testing.T) { nil, false, }, + { + `attr = { "42" = "bar" }`, + "42", + &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 11, Byte: 10}, + End: hcl.Pos{Line: 1, Column: 13, Byte: 12}, + }, + true, + }, + { + `attr = { "foo-bar" = "bar" }`, + "foo-bar", + &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 11, Byte: 10}, + End: hcl.Pos{Line: 1, Column: 18, Byte: 17}, + }, + true, + }, { `attr = { foo.x = "bar" }`, "", @@ -174,21 +195,132 @@ func TestRawObjectKey(t *testing.T) { t.Fatalf("expected to find attribute %q", "attr") } - objConsExpr, ok := attr.Expr.(*hclsyntax.ObjectConsExpr) + items, diags := hcl.ExprMap(attr.Expr) + if diags.HasErrors() { + t.Fatal(diags) + } + + if len(items) != 1 { + t.Fatalf("expected exactly 1 object item, %d given", len(items)) + } + + rawKey, rng, ok := rawObjectKey(items[0].Key) + if !tc.expectedOk && ok { + t.Fatalf("expected parsing to fail, produced: %q at %#v", rawKey, rng) + } + if tc.expectedOk && !ok { + t.Fatalf("expected parsing to succeed and produce %q at %#v", + tc.expectedKey, tc.expectedRange) + } + if tc.expectedKey != rawKey { + t.Fatalf("extracted key mismatch\nexpected: %q\ngiven: %q", tc.expectedKey, rawKey) + } + if diff := cmp.Diff(tc.expectedRange, rng); diff != "" { + t.Fatalf("unexpected range: %s", diff) + } + }) + } +} + +func TestRawObjectKey_json(t *testing.T) { + testCases := []struct { + cfg string + expectedKey string + expectedRange *hcl.Range + expectedOk bool + }{ + { + `{"attr": { "foo": "bar" }}`, + "foo", + &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 12, Byte: 11}, + End: hcl.Pos{Line: 1, Column: 17, Byte: 16}, + }, + true, + }, + { + `{"attr": { "42": "bar" }}`, + "42", + &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 12, Byte: 11}, + End: hcl.Pos{Line: 1, Column: 16, Byte: 15}, + }, + true, + }, + { + `{"attr": { "foo-bar": "bar" }}`, + "foo-bar", + &hcl.Range{ + Filename: "test.hcl.json", + Start: hcl.Pos{Line: 1, Column: 12, Byte: 11}, + End: hcl.Pos{Line: 1, Column: 21, Byte: 20}, + }, + true, + }, + { + `{"attr": { "${foo.x}": "bar" }}`, + "", + nil, + false, + }, + { + `{"attr": { "${(foo)}": "bar" }}`, + "", + nil, + false, + }, + { + `{"attr": { "${(var.foo)}": "bar" }}`, + "", + nil, + false, + }, + { + `{"attr": { "${foo}": "bar" }}`, + "", + nil, + false, + }, + } + for i, tc := range testCases { + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + f, diags := json.ParseWithStartPos([]byte(tc.cfg), "test.hcl.json", hcl.InitialPos) + if len(diags) > 0 { + t.Fatal(diags) + } + + attrs, diags := f.Body.JustAttributes() + if len(diags) > 0 { + t.Fatal(diags) + } + + if len(attrs) != 1 { + t.Fatalf("expected exactly 1 attribute, %d given", len(attrs)) + } + + attr, ok := attrs["attr"] if !ok { - t.Fatalf("expected expression to be ObjectConsExpr, %T given", attr.Expr) + t.Fatalf("expected to find attribute %q", "attr") + } + + items, diags := hcl.ExprMap(attr.Expr) + if diags.HasErrors() { + t.Fatal(diags) } - if len(objConsExpr.Items) != 1 { - t.Fatalf("expected exactly 1 object item, %d given", len(objConsExpr.Items)) + if len(items) != 1 { + t.Fatalf("expected exactly 1 object item, %d given", len(items)) } - rawKey, rng, ok := rawObjectKey(objConsExpr.Items[0].KeyExpr) + rawKey, rng, ok := rawObjectKey(items[0].Key) if !tc.expectedOk && ok { - t.Fatal("expected parsing to fail") + t.Fatalf("expected parsing to fail, produced: %q at %#v", rawKey, rng) } if tc.expectedOk && !ok { - t.Fatal("expected parsing to succeed") + t.Fatalf("expected parsing to succeed and produce %q at %#v", + tc.expectedKey, tc.expectedRange) } if tc.expectedKey != rawKey { t.Fatalf("extracted key mismatch\nexpected: %q\ngiven: %q", tc.expectedKey, rawKey) diff --git a/decoder/reference_targets.go b/decoder/reference_targets.go index 6c6a924d..9454d898 100644 --- a/decoder/reference_targets.go +++ b/decoder/reference_targets.go @@ -277,13 +277,15 @@ func (d *PathDecoder) decodeReferenceTargetsForAttribute(attr *hcl.Attribute, at var targetCtx *TargetContext if attrSchema.Address != nil { attrAddr, ok := resolveAttributeAddress(attr, attrSchema.Address.Steps) - if ok { + if ok && (attrSchema.Address.AsExprType || attrSchema.Address.AsReference) { targetCtx = &TargetContext{ - FriendlyName: attrSchema.Address.FriendlyName, - ScopeId: attrSchema.Address.ScopeId, - AsExprType: attrSchema.Address.AsExprType, - AsReference: attrSchema.Address.AsReference, - ParentAddress: attrAddr, + FriendlyName: attrSchema.Address.FriendlyName, + ScopeId: attrSchema.Address.ScopeId, + AsExprType: attrSchema.Address.AsExprType, + AsReference: attrSchema.Address.AsReference, + ParentAddress: attrAddr, + ParentRangePtr: attr.Range.Ptr(), + ParentDefRangePtr: attr.NameRange.Ptr(), } } } @@ -676,9 +678,21 @@ func bodySchemaAsAttrTypes(bodySchema *schema.BodySchema) map[string]cty.Type { } for name, attr := range bodySchema.Attributes { - attrType, ok := exprConstraintToDataType(attr.Expr) - if ok { - attrTypes[name] = attrType + if attr.Constraint != nil { + cons, ok := attr.Constraint.(schema.TypeAwareConstraint) + if !ok { + continue + } + typ, ok := cons.ConstraintType() + if !ok { + continue + } + attrTypes[name] = typ + } else { + attrType, ok := exprConstraintToDataType(attr.Expr) + if ok { + attrTypes[name] = attrType + } } } @@ -699,17 +713,32 @@ func (d *PathDecoder) collectInferredReferenceTargetsForBody(addr lang.Address, selfRefBodyRangePtr = content.RangePtr } + rawAttributes, _ := body.JustAttributes() + for name, aSchema := range bodySchema.Attributes { var attrType cty.Type if aSchema.Constraint != nil { var ok bool - cons, ok := aSchema.Constraint.(schema.TypeAwareConstraint) - if !ok { - continue - } - attrType, ok = cons.ConstraintType() - if !ok { - continue + rawAttr, ok := rawAttributes[name] + if ok { + // try to infer type if attribute is declared + expr, ok := newExpression(d.pathCtx, rawAttr.Expr, aSchema.Constraint).(CanInferTypeExpression) + if !ok { + continue + } + attrType, ok = expr.InferType() + if !ok { + continue + } + } else { + cons, ok := aSchema.Constraint.(schema.TypeAwareConstraint) + if !ok { + continue + } + attrType, ok = cons.ConstraintType() + if !ok { + continue + } } } else { var ok bool @@ -722,7 +751,7 @@ func (d *PathDecoder) collectInferredReferenceTargetsForBody(addr lang.Address, attrAddr := append(addr.Copy(), lang.AttrStep{Name: name}) - ref := reference.Target{ + legacyRef := reference.Target{ Addr: attrAddr, ScopeId: bAddrSchema.ScopeId, Type: attrType, @@ -741,8 +770,8 @@ func (d *PathDecoder) collectInferredReferenceTargetsForBody(addr lang.Address, copy(localAddr, selfRefAddr) localAddr = append(localAddr, lang.AttrStep{Name: name}) - ref.LocalAddr = localAddr - ref.TargetableFromRangePtr = selfRefBodyRangePtr.Ptr() + legacyRef.LocalAddr = localAddr + legacyRef.TargetableFromRangePtr = selfRefBodyRangePtr.Ptr() targetCtx.ParentLocalAddress = localAddr targetCtx.TargetableFromRangePtr = selfRefBodyRangePtr.Ptr() @@ -750,26 +779,29 @@ func (d *PathDecoder) collectInferredReferenceTargetsForBody(addr lang.Address, var attrExpr hcl.Expression if attr, ok := content.Attributes[name]; ok { - ref.RangePtr = attr.Range.Ptr() - ref.DefRangePtr = attr.NameRange.Ptr() + legacyRef.RangePtr = attr.Range.Ptr() + targetCtx.ParentRangePtr = attr.Range.Ptr() + legacyRef.DefRangePtr = attr.NameRange.Ptr() + targetCtx.ParentDefRangePtr = attr.NameRange.Ptr() attrExpr = attr.Expr } if aSchema.Constraint != nil { - ref.NestedTargets = make(reference.Targets, 0) + if attrExpr == nil { + attrExpr = newEmptyExpressionAtPos(content.RangePtr.Filename, body.MissingItemRange().Start) + } expr, ok := newExpression(d.pathCtx, attrExpr, aSchema.Constraint).(ReferenceTargetsExpression) if ok { ctx := context.Background() - ref.NestedTargets = append(ref.NestedTargets, expr.ReferenceTargets(ctx, targetCtx)...) + refs = append(refs, expr.ReferenceTargets(ctx, targetCtx)...) } } else { if attrExpr != nil && !attrType.IsPrimitiveType() { - ref.NestedTargets = make(reference.Targets, 0) - ref.NestedTargets = append(ref.NestedTargets, decodeReferenceTargetsForComplexTypeExpr(attrAddr, attrExpr, attrType, bAddrSchema.ScopeId)...) + legacyRef.NestedTargets = make(reference.Targets, 0) + legacyRef.NestedTargets = append(legacyRef.NestedTargets, decodeReferenceTargetsForComplexTypeExpr(attrAddr, attrExpr, attrType, bAddrSchema.ScopeId)...) } + refs = append(refs, legacyRef) } - - refs = append(refs, ref) } bTypes := blocksTypesWithSchema(body, bodySchema) diff --git a/decoder/reference_targets_collect_hcl_test.go b/decoder/reference_targets_collect_hcl_test.go index ad8055c5..ee48d17b 100644 --- a/decoder/reference_targets_collect_hcl_test.go +++ b/decoder/reference_targets_collect_hcl_test.go @@ -4732,10 +4732,197 @@ module "different" { `, reference.Targets{}, }, + { + "inferred body targets which are missing", + &schema.BodySchema{ + Blocks: map[string]*schema.BlockSchema{ + "block": { + Address: &schema.BlockAddrSchema{ + Steps: []schema.AddrStep{ + schema.StaticStep{Name: "blk"}, + }, + BodyAsData: true, + InferBody: true, + }, + Body: &schema.BodySchema{ + Attributes: map[string]*schema.AttributeSchema{ + "foo": { + Constraint: schema.LiteralType{ + Type: cty.String, + }, + }, + "bar": { + Constraint: schema.LiteralType{ + Type: cty.Number, + }, + }, + }, + }, + }, + }, + }, + `block { foo = "" }`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "blk"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 19, Byte: 18}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 6, Byte: 5}, + }, + Type: cty.Object(map[string]cty.Type{ + "foo": cty.String, + "bar": cty.Number, + }), + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "blk"}, + lang.AttrStep{Name: "bar"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 9, Byte: 8}, + End: hcl.Pos{Line: 1, Column: 9, Byte: 8}, + }, + Type: cty.Number, + }, + { + Addr: lang.Address{ + lang.RootStep{Name: "blk"}, + lang.AttrStep{Name: "foo"}, + }, + RangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 9, Byte: 8}, + End: hcl.Pos{Line: 1, Column: 17, Byte: 16}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 9, Byte: 8}, + End: hcl.Pos{Line: 1, Column: 12, Byte: 11}, + }, + Type: cty.String, + }, + }, + }, + }, + }, + { + "dependent inferred body", + &schema.BodySchema{ + Blocks: map[string]*schema.BlockSchema{ + "provider": { + Labels: []*schema.LabelSchema{ + {Name: "name", IsDepKey: true}, + }, + Address: &schema.BlockAddrSchema{ + Steps: []schema.AddrStep{ + schema.LabelStep{Index: 0}, + }, + DependentBodyAsData: true, + ScopeId: lang.ScopeId("test"), + InferDependentBody: true, + }, + Type: schema.BlockTypeObject, + DependentBody: map[schema.SchemaKey]*schema.BodySchema{ + schema.NewSchemaKey(schema.DependencyKeys{ + Labels: []schema.LabelDependent{ + {Index: 0, Value: "aws"}, + }, + }): { + Attributes: map[string]*schema.AttributeSchema{ + "attr_map": { + Constraint: schema.LiteralType{Type: cty.Map(cty.String)}, + IsOptional: true, + }, + }, + }, + }, + }, + }, + }, + `provider "aws" { + attr_map = { + foo = "bar" + } +} +`, + reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "aws"}, + }, + LocalAddr: lang.Address{}, + ScopeId: "test", + RangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 5, Column: 2, Byte: 53}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 15, Byte: 14}, + }, + Type: cty.Object(map[string]cty.Type{ + "attr_map": cty.Map(cty.String), + }), + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "aws"}, + lang.AttrStep{Name: "attr_map"}, + }, + ScopeId: lang.ScopeId("test"), + RangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 19}, + End: hcl.Pos{Line: 4, Column: 4, Byte: 51}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 19}, + End: hcl.Pos{Line: 2, Column: 11, Byte: 27}, + }, + Type: cty.Map(cty.String), + NestedTargets: reference.Targets{ + { + Addr: lang.Address{ + lang.RootStep{Name: "aws"}, + lang.AttrStep{Name: "attr_map"}, + lang.IndexStep{Key: cty.StringVal("foo")}, + }, + ScopeId: lang.ScopeId("test"), + RangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 3, Column: 5, Byte: 36}, + End: hcl.Pos{Line: 3, Column: 16, Byte: 47}, + }, + DefRangePtr: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 3, Column: 5, Byte: 36}, + End: hcl.Pos{Line: 3, Column: 8, Byte: 39}, + }, + Type: cty.String, + }, + }, + }, + }, + }, + }, + }, } for i, tc := range testCases { - t.Run(fmt.Sprintf("%d-%s", i, tc.name), func(t *testing.T) { + t.Run(fmt.Sprintf("%2d-%s", i, tc.name), func(t *testing.T) { f, _ := hclsyntax.ParseConfig([]byte(tc.cfg), "test.tf", hcl.InitialPos) d := testPathDecoder(t, &PathContext{ diff --git a/schema/attribute_schema_test.go b/schema/attribute_schema_test.go index 9410cac5..08f1598f 100644 --- a/schema/attribute_schema_test.go +++ b/schema/attribute_schema_test.go @@ -87,7 +87,7 @@ func TestAttributeSchema_Validate(t *testing.T) { Constraint: Reference{Address: &ReferenceAddrSchema{}}, IsOptional: true, }, - errors.New("Constraint: schema.Reference: Address requires non-emmpty ScopeId"), + errors.New("Constraint: schema.Reference: Address requires non-empty ScopeId"), }, { &AttributeSchema{ diff --git a/schema/constraint_reference.go b/schema/constraint_reference.go index 2d181f00..888ab88c 100644 --- a/schema/constraint_reference.go +++ b/schema/constraint_reference.go @@ -79,7 +79,7 @@ func (ref Reference) Validate() error { return errors.New("cannot have both Address and OfType/OfScopeId set") } if ref.Address != nil && ref.Address.ScopeId == "" { - return errors.New("Address requires non-emmpty ScopeId") + return errors.New("Address requires non-empty ScopeId") } return nil }