Skip to content

Commit 9a81ac7

Browse files
authored
Support Dynamic block expression extensions (#139)
Adds support for completion of dynamic blocks in resource, data, provider and provisioner blocks. This does not provide completion for the label for a given dynamic block, which will be provided in a future set of work.
1 parent 628a057 commit 9a81ac7

File tree

10 files changed

+526
-18
lines changed

10 files changed

+526
-18
lines changed

decoder/body_candidates.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ func (d *PathDecoder) bodySchemaCandidates(body *hclsyntax.Body, schema *schema.
3434
candidates.List = append(candidates.List, attributeSchemaToCandidate("for_each", forEachAttributeSchema(), editRng))
3535
}
3636
}
37+
38+
if schema.Extensions.DynamicBlocks {
39+
candidates.List = append(candidates.List, d.blockSchemaToCandidate("dynamic", dynamicBlockSchema(), editRng))
40+
}
3741
}
3842

3943
if len(schema.Attributes) > 0 {

decoder/body_extensions_test.go

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -985,3 +985,224 @@ for_each =
985985
})
986986
}
987987
}
988+
989+
func TestCompletionAtPos_BodySchema_DynamicBlock_Extensions(t *testing.T) {
990+
ctx := context.Background()
991+
992+
testCases := []struct {
993+
testName string
994+
bodySchema *schema.BodySchema
995+
cfg string
996+
pos hcl.Pos
997+
expectedCandidates lang.Candidates
998+
}{
999+
{
1000+
"dynamic block does not complete if not enabled",
1001+
&schema.BodySchema{
1002+
Blocks: map[string]*schema.BlockSchema{
1003+
"resource": {
1004+
Labels: []*schema.LabelSchema{
1005+
{
1006+
Name: "type",
1007+
},
1008+
{
1009+
Name: "name",
1010+
},
1011+
},
1012+
Body: &schema.BodySchema{
1013+
Extensions: &schema.BodyExtensions{
1014+
DynamicBlocks: false,
1015+
},
1016+
},
1017+
},
1018+
},
1019+
},
1020+
`
1021+
resource "aws_elastic_beanstalk_environment" "example" {
1022+
name = "example"
1023+
1024+
}`,
1025+
hcl.Pos{
1026+
Line: 4,
1027+
Column: 3,
1028+
Byte: 77,
1029+
},
1030+
lang.CompleteCandidates([]lang.Candidate{}),
1031+
},
1032+
{
1033+
"dynamic block completion",
1034+
&schema.BodySchema{
1035+
Blocks: map[string]*schema.BlockSchema{
1036+
"resource": {
1037+
Labels: []*schema.LabelSchema{
1038+
{Name: "type"}, {Name: "name"},
1039+
},
1040+
Body: &schema.BodySchema{
1041+
Extensions: &schema.BodyExtensions{
1042+
DynamicBlocks: true,
1043+
},
1044+
},
1045+
},
1046+
},
1047+
},
1048+
`resource "aws_elastic_beanstalk_environment" "example" {
1049+
name = "example"
1050+
1051+
}`,
1052+
hcl.Pos{
1053+
Line: 3,
1054+
Column: 3,
1055+
Byte: 76,
1056+
},
1057+
lang.CompleteCandidates([]lang.Candidate{
1058+
{
1059+
Label: "dynamic",
1060+
Description: lang.MarkupContent{
1061+
Value: "A dynamic block to produce blocks dynamically by iterating over a given complex value",
1062+
Kind: lang.MarkdownKind,
1063+
},
1064+
Detail: "Block, map",
1065+
Kind: lang.BlockCandidateKind,
1066+
TextEdit: lang.TextEdit{
1067+
Range: hcl.Range{
1068+
Filename: "test.tf",
1069+
Start: hcl.Pos{
1070+
Line: 3,
1071+
Column: 3,
1072+
Byte: 76,
1073+
},
1074+
End: hcl.Pos{
1075+
Line: 3,
1076+
Column: 3,
1077+
Byte: 76,
1078+
},
1079+
},
1080+
NewText: "dynamic",
1081+
Snippet: "dynamic \"${1:name}\" {\n ${2}\n}",
1082+
},
1083+
},
1084+
}),
1085+
},
1086+
{
1087+
"dynamic block inner completion",
1088+
&schema.BodySchema{
1089+
Blocks: map[string]*schema.BlockSchema{
1090+
"resource": {
1091+
Labels: []*schema.LabelSchema{
1092+
{Name: "type"}, {Name: "name"},
1093+
},
1094+
Body: &schema.BodySchema{
1095+
Extensions: &schema.BodyExtensions{
1096+
DynamicBlocks: true,
1097+
},
1098+
},
1099+
},
1100+
},
1101+
},
1102+
`resource "aws_elastic_beanstalk_environment" "example" {
1103+
name = "example"
1104+
dynamic "foo" {
1105+
1106+
}
1107+
}`,
1108+
hcl.Pos{Line: 4, Column: 5, Byte: 94},
1109+
lang.CompleteCandidates([]lang.Candidate{
1110+
{
1111+
Label: "content",
1112+
Description: lang.MarkupContent{
1113+
Value: "The body of each generated block",
1114+
Kind: lang.PlainTextKind,
1115+
},
1116+
Detail: "Block, max: 1",
1117+
Kind: lang.BlockCandidateKind,
1118+
TextEdit: lang.TextEdit{
1119+
Range: hcl.Range{
1120+
Filename: "test.tf",
1121+
Start: hcl.Pos{Line: 4, Column: 5, Byte: 94},
1122+
End: hcl.Pos{Line: 4, Column: 5, Byte: 94},
1123+
},
1124+
NewText: "content",
1125+
Snippet: "content {\n ${1}\n}",
1126+
},
1127+
},
1128+
{
1129+
Label: "for_each",
1130+
Description: lang.MarkupContent{
1131+
Value: "A meta-argument that accepts a map or a set of strings, and creates an instance for each item in that map or set.\n\n**Note**: A given block cannot use both `count` and `for_each`.",
1132+
Kind: lang.MarkdownKind,
1133+
},
1134+
Detail: "required, map of any single type or set of string",
1135+
Kind: lang.AttributeCandidateKind,
1136+
TriggerSuggest: true,
1137+
TextEdit: lang.TextEdit{
1138+
Range: hcl.Range{
1139+
Filename: "test.tf",
1140+
Start: hcl.Pos{Line: 4, Column: 5, Byte: 94},
1141+
End: hcl.Pos{Line: 4, Column: 5, Byte: 94},
1142+
},
1143+
NewText: "for_each",
1144+
Snippet: "for_each = ",
1145+
},
1146+
},
1147+
{
1148+
Label: "iterator",
1149+
Description: lang.MarkupContent{
1150+
Value: "The name of a temporary variable that represents the current element of the complex value. Defaults to the label of the dynamic block.",
1151+
Kind: lang.MarkdownKind,
1152+
},
1153+
Detail: "optional, string",
1154+
Kind: lang.AttributeCandidateKind,
1155+
TextEdit: lang.TextEdit{
1156+
Range: hcl.Range{
1157+
Filename: "test.tf",
1158+
Start: hcl.Pos{Line: 4, Column: 5, Byte: 94},
1159+
End: hcl.Pos{Line: 4, Column: 5, Byte: 94},
1160+
},
1161+
NewText: "iterator",
1162+
Snippet: `iterator = "${1:value}"`,
1163+
},
1164+
},
1165+
{
1166+
Label: "labels",
1167+
Description: lang.MarkupContent{
1168+
Value: "A list of strings that specifies the block labels, in order, to use for each generated block.",
1169+
Kind: lang.MarkdownKind,
1170+
},
1171+
Detail: "optional, list of string",
1172+
Kind: lang.AttributeCandidateKind,
1173+
TextEdit: lang.TextEdit{
1174+
Range: hcl.Range{
1175+
Filename: "test.tf",
1176+
Start: hcl.Pos{Line: 4, Column: 5, Byte: 94},
1177+
End: hcl.Pos{Line: 4, Column: 5, Byte: 94},
1178+
},
1179+
NewText: "labels",
1180+
Snippet: "labels = [\n ${0}\n]",
1181+
},
1182+
},
1183+
}),
1184+
},
1185+
}
1186+
1187+
for i, tc := range testCases {
1188+
t.Run(fmt.Sprintf("%d-%s", i, tc.testName), func(t *testing.T) {
1189+
f, _ := hclsyntax.ParseConfig([]byte(tc.cfg), "test.tf", hcl.InitialPos)
1190+
1191+
d := testPathDecoder(t, &PathContext{
1192+
Schema: tc.bodySchema,
1193+
Files: map[string]*hcl.File{
1194+
"test.tf": f,
1195+
},
1196+
})
1197+
1198+
candidates, err := d.CandidatesAtPos(ctx, "test.tf", tc.pos)
1199+
if err != nil {
1200+
t.Fatal(err)
1201+
}
1202+
1203+
if diff := cmp.Diff(tc.expectedCandidates, candidates); diff != "" {
1204+
t.Fatalf("unexpected candidates: %s", diff)
1205+
}
1206+
})
1207+
}
1208+
}

decoder/candidates.go

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ func (d *PathDecoder) candidatesAtPos(ctx context.Context, body *hclsyntax.Body,
6262
ctx = schema.WithActiveForEach(ctx)
6363
}
6464
}
65+
66+
if bodySchema.Extensions.DynamicBlocks {
67+
ctx = schema.WithActiveDynamicBlock(ctx)
68+
}
6569
}
6670

6771
for _, attr := range body.Attributes {
@@ -99,12 +103,18 @@ func (d *PathDecoder) candidatesAtPos(ctx context.Context, body *hclsyntax.Body,
99103

100104
for _, block := range body.Blocks {
101105
if block.Range().ContainsPos(pos) {
102-
bSchema, ok := bodySchema.Blocks[block.Type]
103-
if !ok {
104-
return lang.ZeroCandidates(), &PositionalError{
105-
Filename: filename,
106-
Pos: pos,
107-
Msg: fmt.Sprintf("unknown block type %q", block.Type),
106+
var bSchema *schema.BlockSchema
107+
if bodySchema.Extensions != nil && bodySchema.Extensions.DynamicBlocks && block.Type == "dynamic" {
108+
bSchema = dynamicBlockSchema()
109+
} else {
110+
var ok bool
111+
bSchema, ok = bodySchema.Blocks[block.Type]
112+
if !ok {
113+
return lang.ZeroCandidates(), &PositionalError{
114+
Filename: filename,
115+
Pos: pos,
116+
Msg: fmt.Sprintf("unknown block type %q", block.Type),
117+
}
108118
}
109119
}
110120

decoder/decoder.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,56 @@ func forEachAttributeSchema() *schema.AttributeSchema {
195195
}
196196
}
197197

198+
func dynamicBlockSchema() *schema.BlockSchema {
199+
return &schema.BlockSchema{
200+
Description: lang.Markdown("A dynamic block to produce blocks dynamically by iterating over a given complex value"),
201+
Type: schema.BlockTypeMap,
202+
Labels: []*schema.LabelSchema{
203+
{Name: "name"},
204+
},
205+
Body: &schema.BodySchema{
206+
Attributes: map[string]*schema.AttributeSchema{
207+
"for_each": {
208+
Expr: schema.ExprConstraints{
209+
schema.TraversalExpr{OfType: cty.Map(cty.DynamicPseudoType)},
210+
schema.TraversalExpr{OfType: cty.Set(cty.String)},
211+
schema.LiteralTypeExpr{Type: cty.Map(cty.DynamicPseudoType)},
212+
schema.LiteralTypeExpr{Type: cty.Set(cty.String)},
213+
},
214+
IsRequired: true,
215+
Description: lang.Markdown("A meta-argument that accepts a map or a set of strings, and creates an instance for each item in that map or set.\n\n" +
216+
"**Note**: A given block cannot use both `count` and `for_each`."),
217+
},
218+
"iterator": {
219+
Expr: schema.LiteralTypeOnly(cty.String),
220+
IsOptional: true,
221+
Description: lang.Markdown("The name of a temporary variable that represents the current " +
222+
"element of the complex value. Defaults to the label of the dynamic block."),
223+
},
224+
"labels": {
225+
Expr: schema.ExprConstraints{
226+
schema.ListExpr{
227+
Elem: schema.ExprConstraints{
228+
schema.LiteralTypeExpr{Type: cty.String},
229+
schema.TraversalExpr{OfType: cty.String},
230+
},
231+
},
232+
},
233+
IsOptional: true,
234+
Description: lang.Markdown("A list of strings that specifies the block labels, " +
235+
"in order, to use for each generated block."),
236+
},
237+
},
238+
Blocks: map[string]*schema.BlockSchema{
239+
"content": {
240+
Description: lang.PlainText("The body of each generated block"),
241+
MaxItems: 1,
242+
},
243+
},
244+
},
245+
}
246+
}
247+
198248
func countIndexHoverData(rng hcl.Range) *lang.HoverData {
199249
return &lang.HoverData{
200250
Content: lang.Markdown("`count.index` _number_\n\nThe distinct index number (starting with 0) corresponding to the instance"),

decoder/hover.go

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ func (d *PathDecoder) hoverAtPos(ctx context.Context, body *hclsyntax.Body, body
5555
if bodySchema.Extensions.ForEach {
5656
ctx = schema.WithActiveForEach(ctx)
5757
}
58+
59+
if bodySchema.Extensions.DynamicBlocks {
60+
ctx = schema.WithActiveDynamicBlock(ctx)
61+
}
5862
}
5963

6064
for name, attr := range body.Attributes {
@@ -103,12 +107,18 @@ func (d *PathDecoder) hoverAtPos(ctx context.Context, body *hclsyntax.Body, body
103107

104108
for _, block := range body.Blocks {
105109
if block.Range().ContainsPos(pos) {
106-
bSchema, ok := bodySchema.Blocks[block.Type]
107-
if !ok {
108-
return nil, &PositionalError{
109-
Filename: filename,
110-
Pos: pos,
111-
Msg: fmt.Sprintf("unknown block type %q", block.Type),
110+
var bSchema *schema.BlockSchema
111+
if bodySchema.Extensions != nil && bodySchema.Extensions.DynamicBlocks && block.Type == "dynamic" {
112+
bSchema = dynamicBlockSchema()
113+
} else {
114+
var ok bool
115+
bSchema, ok = bodySchema.Blocks[block.Type]
116+
if !ok {
117+
return nil, &PositionalError{
118+
Filename: filename,
119+
Pos: pos,
120+
Msg: fmt.Sprintf("unknown block type %q", block.Type),
121+
}
112122
}
113123
}
114124

0 commit comments

Comments
 (0)