diff --git a/decoder/attribute_candidates.go b/decoder/attribute_candidates.go index 8d4c8525..0c7d00f5 100644 --- a/decoder/attribute_candidates.go +++ b/decoder/attribute_candidates.go @@ -30,15 +30,17 @@ func detailForAttribute(attr *schema.AttributeSchema) string { var detail string if attr.IsRequired { detail = "Required" - } else { + } else if attr.IsOptional { detail = "Optional" } - ec := ExprConstraints(attr.Expr) - names := ec.FriendlyNames() - - if len(names) > 0 { - detail += fmt.Sprintf(", %s", strings.Join(names, " or ")) + friendlyName := attr.Expr.FriendlyName() + if friendlyName != "" { + if detail != "" { + detail = strings.Join([]string{detail, friendlyName}, ", ") + } else { + detail = friendlyName + } } return detail diff --git a/decoder/body_decoder_test.go b/decoder/body_decoder_test.go index e01ac442..32d17a4f 100644 --- a/decoder/body_decoder_test.go +++ b/decoder/body_decoder_test.go @@ -56,7 +56,7 @@ func TestDecoder_CandidateAtPos_incompleteAttributes(t *testing.T) { List: []lang.Candidate{ { Label: "attr1", - Detail: "Optional, number", + Detail: "number", TextEdit: lang.TextEdit{ Range: hcl.Range{ Filename: "test.tf", diff --git a/decoder/candidates_test.go b/decoder/candidates_test.go index b54bdbf8..8361c219 100644 --- a/decoder/candidates_test.go +++ b/decoder/candidates_test.go @@ -664,8 +664,8 @@ func TestDecoder_CandidatesAtPos_basic(t *testing.T) { }): { Attributes: map[string]*schema.AttributeSchema{ "one": {Expr: schema.LiteralTypeOnly(cty.String), IsRequired: true}, - "two": {Expr: schema.LiteralTypeOnly(cty.Number)}, - "three": {Expr: schema.LiteralTypeOnly(cty.Bool)}, + "two": {Expr: schema.LiteralTypeOnly(cty.Number), IsOptional: true}, + "three": {Expr: schema.LiteralTypeOnly(cty.Bool), IsOptional: true}, }, }, schema.NewSchemaKey(schema.DependencyKeys{ @@ -944,7 +944,7 @@ func TestDecoder_CandidatesAtPos_AnyAttribute(t *testing.T) { expectedCandidates := lang.CompleteCandidates([]lang.Candidate{ { Label: "name", - Detail: "Optional, object", + Detail: "object", TextEdit: lang.TextEdit{ Range: hcl.Range{ Filename: "test.tf", @@ -979,6 +979,7 @@ func TestDecoder_CandidatesAtPos_multipleTypes(t *testing.T) { schema.LiteralTypeExpr{Type: cty.Set(cty.DynamicPseudoType)}, schema.LiteralTypeExpr{Type: cty.Map(cty.DynamicPseudoType)}, }, + IsOptional: true, }, }, }, @@ -1045,7 +1046,7 @@ func TestDecoder_CandidatesAtPos_incompleteAttrOrBlock(t *testing.T) { Labels: resourceLabelSchema, Body: &schema.BodySchema{ Attributes: map[string]*schema.AttributeSchema{ - "count": {Expr: schema.LiteralTypeOnly(cty.Number)}, + "count": {Expr: schema.LiteralTypeOnly(cty.Number), IsOptional: true}, }, }, } @@ -1173,7 +1174,7 @@ func TestDecoder_CandidatesAtPos_incompleteLabel(t *testing.T) { Labels: resourceLabelSchema, Body: &schema.BodySchema{ Attributes: map[string]*schema.AttributeSchema{ - "count": {Expr: schema.LiteralTypeOnly(cty.Number)}, + "count": {Expr: schema.LiteralTypeOnly(cty.Number), IsOptional: true}, }, }, DependentBody: map[schema.SchemaKey]*schema.BodySchema{ diff --git a/decoder/expression_candidates.go b/decoder/expression_candidates.go index ff95dd2e..f2824a94 100644 --- a/decoder/expression_candidates.go +++ b/decoder/expression_candidates.go @@ -56,6 +56,37 @@ func constraintsAtPos(expr hcl.Expression, constraints ExprConstraints, pos hcl. Filename: eType.Range().Filename, } } + case *hclsyntax.ObjectConsExpr: + oe, ok := constraints.ObjectExpr() + if ok { + undeclaredAttributes := oe.Attributes + for _, item := range eType.Items { + key, _ := item.KeyExpr.Value(nil) + if !key.IsWhollyKnown() || key.Type() != cty.String { + continue + } + attr, ok := oe.Attributes[key.AsString()] + if !ok { + // unknown attribute + continue + } + delete(undeclaredAttributes, key.AsString()) + + itemRng := hcl.RangeBetween(item.KeyExpr.Range(), item.ValueExpr.Range()) + if item.ValueExpr.Range().ContainsPos(pos) { + return constraintsAtPos(item.ValueExpr, ExprConstraints(attr.Expr), pos) + } else if itemRng.ContainsPos(pos) { + // middle of attribute name or equal sign + return ExprConstraints{}, expr.Range() + } + } + + return ExprConstraints{undeclaredAttributes}, hcl.Range{ + Start: pos, + End: pos, + Filename: eType.Range().Filename, + } + } } return ExprConstraints{}, expr.Range() @@ -96,9 +127,10 @@ func constraintToCandidates(constraint schema.ExprConstraint, editRng hcl.Range) }) case schema.TupleConsExpr: candidates = append(candidates, lang.Candidate{ - Label: fmt.Sprintf(`[%s]`, labelForConstraints(c.AnyElem)), - Detail: c.Name, - Kind: lang.TupleCandidateKind, + Label: fmt.Sprintf(`[%s]`, labelForConstraints(c.AnyElem)), + Detail: c.Name, + Description: c.Description, + Kind: lang.TupleCandidateKind, TextEdit: lang.TextEdit{ NewText: `[ ]`, Snippet: `[ ${0} ]`, @@ -108,9 +140,10 @@ func constraintToCandidates(constraint schema.ExprConstraint, editRng hcl.Range) }) case schema.MapExpr: candidates = append(candidates, lang.Candidate{ - Label: fmt.Sprintf(`{ key =%s}`, labelForConstraints(c.Elem)), - Detail: c.FriendlyName(), - Kind: lang.MapCandidateKind, + Label: fmt.Sprintf(`{ key =%s}`, labelForConstraints(c.Elem)), + Detail: c.FriendlyName(), + Description: c.Description, + Kind: lang.MapCandidateKind, TextEdit: lang.TextEdit{ NewText: fmt.Sprintf("{\n name = %s\n}", newTextForConstraints(c.Elem, true)), @@ -120,6 +153,35 @@ func constraintToCandidates(constraint schema.ExprConstraint, editRng hcl.Range) }, TriggerSuggest: len(c.Elem) > 0, }) + case schema.ObjectExpr: + candidates = append(candidates, lang.Candidate{ + Label: `{ }`, + Detail: c.FriendlyName(), + Description: c.Description, + Kind: lang.ObjectCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "{\n \n}", + Snippet: "{\n ${1}\n}", + Range: editRng, + }, + TriggerSuggest: len(c.Attributes) > 0, + }) + case schema.ObjectExprAttributes: + attrNames := sortedObjectExprAttrNames(c) + for _, name := range attrNames { + attr := c[name] + candidates = append(candidates, lang.Candidate{ + Label: name, + Detail: attr.Expr.FriendlyName(), + Description: attr.Description, + Kind: lang.AttributeCandidateKind, + TextEdit: lang.TextEdit{ + NewText: fmt.Sprintf("%s = %s", name, newTextForConstraints(attr.Expr, true)), + Snippet: fmt.Sprintf("%s = %s", name, snippetForConstraints(1, attr.Expr, true)), + Range: editRng, + }, + }) + } } return candidates @@ -141,6 +203,8 @@ func newTextForConstraints(cons schema.ExprConstraints, isNested bool) string { return fmt.Sprintf("[\n %s\n]", newTextForConstraints(c.AnyElem, true)) case schema.MapExpr: return fmt.Sprintf("{\n %s\n}", newTextForConstraints(c.Elem, true)) + case schema.ObjectExpr: + return "{\n \n}" } } return "" @@ -162,6 +226,8 @@ func snippetForConstraints(placeholder uint, cons schema.ExprConstraints, isNest return fmt.Sprintf("[\n %s\n]", snippetForConstraints(placeholder+1, c.AnyElem, true)) case schema.MapExpr: return fmt.Sprintf("{\n %s\n}", snippetForConstraints(placeholder+1, c.Elem, true)) + case schema.ObjectExpr: + return fmt.Sprintf("{\n ${%d}\n}", placeholder+1) } } return "" @@ -319,6 +385,8 @@ func snippetForExprContraints(placeholder uint, ec schema.ExprConstraints) strin return fmt.Sprintf("{\n ${%d:name} = %s\n }", placeholder, snippetForExprContraints(placeholder+1, et.Elem)) + case schema.ObjectExpr: + return fmt.Sprintf("{\n ${%d}\n }", placeholder+1) } return "" } diff --git a/decoder/expression_candidates_test.go b/decoder/expression_candidates_test.go index f15a27a2..7ae63dd2 100644 --- a/decoder/expression_candidates_test.go +++ b/decoder/expression_candidates_test.go @@ -124,6 +124,172 @@ func TestDecoder_CandidateAtPos_expressions(t *testing.T) { }, }), }, + { + "object as expression", + map[string]*schema.AttributeSchema{ + "attr": { + Expr: schema.ExprConstraints{ + schema.ObjectExpr{ + Attributes: schema.ObjectExprAttributes{ + "first": schema.ObjectAttribute{ + Expr: schema.LiteralTypeOnly(cty.String), + }, + "second": schema.ObjectAttribute{ + Expr: schema.LiteralTypeOnly(cty.Number), + }, + }, + }, + }, + }, + }, + `attr = +`, + hcl.Pos{Line: 1, Column: 8, Byte: 7}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: "{ }", + Detail: "object", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 1, + Column: 8, + Byte: 7, + }, + End: hcl.Pos{ + Line: 1, + Column: 8, + Byte: 7, + }, + }, + NewText: "{\n \n}", + Snippet: "{\n ${1}\n}", + }, + Kind: lang.ObjectCandidateKind, + TriggerSuggest: true, + }, + }), + }, + { + "object as expression - attribute", + map[string]*schema.AttributeSchema{ + "attr": { + Expr: schema.ExprConstraints{ + schema.ObjectExpr{ + Attributes: schema.ObjectExprAttributes{ + "first": schema.ObjectAttribute{ + Expr: schema.LiteralTypeOnly(cty.String), + }, + "second": schema.ObjectAttribute{ + Expr: schema.LiteralTypeOnly(cty.Number), + }, + }, + }, + }, + }, + }, + `attr = { + +} +`, + hcl.Pos{Line: 2, Column: 3, Byte: 11}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: "first", + Detail: "string", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 2, + Column: 3, + Byte: 11, + }, + End: hcl.Pos{ + Line: 2, + Column: 3, + Byte: 11, + }, + }, + NewText: `first = ""`, + Snippet: `first = "${1:value}"`, + }, + Kind: lang.AttributeCandidateKind, + }, + { + Label: "second", + Detail: "number", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 2, + Column: 3, + Byte: 11, + }, + End: hcl.Pos{ + Line: 2, + Column: 3, + Byte: 11, + }, + }, + NewText: "second = 1", + Snippet: "second = ${1:1}", + }, + Kind: lang.AttributeCandidateKind, + }, + }), + }, + { + "object as expression - attributes partially declared", + map[string]*schema.AttributeSchema{ + "attr": { + Expr: schema.ExprConstraints{ + schema.ObjectExpr{ + Attributes: schema.ObjectExprAttributes{ + "first": schema.ObjectAttribute{ + Expr: schema.LiteralTypeOnly(cty.String), + }, + "second": schema.ObjectAttribute{ + Expr: schema.LiteralTypeOnly(cty.Number), + }, + }, + }, + }, + }, + }, + `attr = { + first = "blah" + +} +`, + hcl.Pos{Line: 3, Column: 3, Byte: 28}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: "second", + Detail: "number", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 3, + Column: 3, + Byte: 28, + }, + End: hcl.Pos{ + Line: 3, + Column: 3, + Byte: 28, + }, + }, + NewText: "second = 1", + Snippet: "second = ${1:1}", + }, + Kind: lang.AttributeCandidateKind, + }, + }), + }, { "list as value", map[string]*schema.AttributeSchema{ diff --git a/decoder/expression_constraints.go b/decoder/expression_constraints.go index 2fbac3ab..25f87819 100644 --- a/decoder/expression_constraints.go +++ b/decoder/expression_constraints.go @@ -8,26 +8,6 @@ import ( type ExprConstraints schema.ExprConstraints -func (ec ExprConstraints) FriendlyNames() []string { - names := make([]string, 0) - for _, constraint := range ec { - if name := constraint.FriendlyName(); name != "" && - !namesContain(names, name) { - names = append(names, name) - } - } - return names -} - -func namesContain(names []string, name string) bool { - for _, n := range names { - if n == name { - return true - } - } - return false -} - func (ec ExprConstraints) HasKeywordsOnly() bool { hasKeywordExpr := false for _, constraint := range ec { @@ -58,6 +38,15 @@ func (ec ExprConstraints) MapExpr() (schema.MapExpr, bool) { return schema.MapExpr{}, false } +func (ec ExprConstraints) ObjectExpr() (schema.ObjectExpr, bool) { + for _, c := range ec { + if me, ok := c.(schema.ObjectExpr); ok { + return me, ok + } + } + return schema.ObjectExpr{}, false +} + func (ec ExprConstraints) TupleConsExpr() (schema.TupleConsExpr, bool) { for _, c := range ec { if tc, ok := c.(schema.TupleConsExpr); ok { diff --git a/decoder/hover.go b/decoder/hover.go index 6bba237e..c9c685f0 100644 --- a/decoder/hover.go +++ b/decoder/hover.go @@ -2,6 +2,7 @@ package decoder import ( "fmt" + "sort" "strings" "github.com/hashicorp/hcl-lang/lang" @@ -67,7 +68,7 @@ func (d *Decoder) hoverAtPos(body *hclsyntax.Body, bodySchema *schema.BodySchema if attr.Expr.Range().ContainsPos(pos) { exprCons := ExprConstraints(aSchema.Expr) - content, err := hoverContentForExpr(attr.Expr, exprCons) + data, err := hoverDataForExpr(attr.Expr, exprCons, pos) if err != nil { return nil, &PositionalError{ Filename: filename, @@ -75,10 +76,7 @@ func (d *Decoder) hoverAtPos(body *hclsyntax.Body, bodySchema *schema.BodySchema Msg: err.Error(), } } - return &lang.HoverData{ - Content: lang.Markdown(content), - Range: attr.Expr.Range(), - }, nil + return data, nil } } } @@ -212,28 +210,61 @@ func hoverContentForBlock(bType string, schema *schema.BlockSchema) lang.MarkupC } } -func hoverContentForExpr(expr hcl.Expression, constraints ExprConstraints) (string, error) { +func hoverDataForExpr(expr hcl.Expression, constraints ExprConstraints, pos hcl.Pos) (*lang.HoverData, error) { switch e := expr.(type) { case *hclsyntax.ScopeTraversalExpr: kw, ok := constraints.KeywordExpr() if ok && len(e.Traversal) == 1 { - return fmt.Sprintf("`%s` _%s_", kw.Keyword, kw.FriendlyName()), nil + return &lang.HoverData{ + Content: lang.Markdown(fmt.Sprintf("`%s` _%s_", kw.Keyword, kw.FriendlyName())), + Range: expr.Range(), + }, nil } case *hclsyntax.TemplateExpr: if e.IsStringLiteral() { - return hoverContentForExpr(e.Parts[0], constraints) + data, err := hoverDataForExpr(e.Parts[0], constraints, pos) + if err != nil { + return nil, err + } + // Account for the enclosing quotes + return &lang.HoverData{ + Content: data.Content, + Range: expr.Range(), + }, nil } if v, ok := stringValFromTemplateExpr(e); ok { if constraints.HasLiteralTypeOf(cty.String) { - return hoverContentForValue(v, 0) + content, err := hoverContentForValue(v, 0) + if err != nil { + return nil, err + } + return &lang.HoverData{ + Content: lang.Markdown(content), + Range: expr.Range(), + }, nil } lv, ok := constraints.LiteralValueOf(v) if ok { - return hoverContentForValue(lv.Val, 0) + content, err := hoverContentForValue(lv.Val, 0) + if err != nil { + return nil, err + } + return &lang.HoverData{ + Content: lang.Markdown(content), + Range: expr.Range(), + }, nil } } case *hclsyntax.TemplateWrapExpr: - return hoverContentForExpr(e.Wrapped, constraints) + data, err := hoverDataForExpr(e.Wrapped, constraints, pos) + if err != nil { + return nil, err + } + // Account for the enclosing quotes + return &lang.HoverData{ + Content: data.Content, + Range: expr.Range(), + }, nil case *hclsyntax.TupleConsExpr: tupleCons, ok := constraints.TupleConsExpr() if ok { @@ -241,46 +272,171 @@ func hoverContentForExpr(expr hcl.Expression, constraints ExprConstraints) (stri if tupleCons.Description.Value != "" { content += "\n\n" + tupleCons.Description.Value } - return content, nil + return &lang.HoverData{ + Content: lang.Markdown(content), + Range: expr.Range(), + }, nil } lt, ok := constraints.LiteralTypeOfTupleExpr() if ok { - return hoverContentForType(lt.Type) + content, err := hoverContentForType(lt.Type) + if err != nil { + return nil, err + } + return &lang.HoverData{ + Content: lang.Markdown(content), + Range: expr.Range(), + }, nil } litVal, ok := constraints.LiteralValueOfTupleExpr(e) if ok { - return hoverContentForValue(litVal.Val, 0) + content, err := hoverContentForValue(litVal.Val, 0) + if err != nil { + return nil, err + } + return &lang.HoverData{ + Content: lang.Markdown(content), + Range: expr.Range(), + }, nil } case *hclsyntax.ObjectConsExpr: + objExpr, ok := constraints.ObjectExpr() + if ok { + return hoverDataForObjectExpr(e, objExpr, pos) + } mapExpr, ok := constraints.MapExpr() if ok { content := fmt.Sprintf("_%s_", mapExpr.FriendlyName()) if mapExpr.Description.Value != "" { content += "\n\n" + mapExpr.Description.Value } - return content, nil + return &lang.HoverData{ + Content: lang.Markdown(content), + Range: expr.Range(), + }, nil } lt, ok := constraints.LiteralTypeOfObjectConsExpr() if ok { - return hoverContentForType(lt.Type) + content, err := hoverContentForType(lt.Type) + if err != nil { + return nil, err + } + return &lang.HoverData{ + Content: lang.Markdown(content), + Range: expr.Range(), + }, nil } litVal, ok := constraints.LiteralValueOfObjectConsExpr(e) if ok { - return hoverContentForValue(litVal.Val, 0) + content, err := hoverContentForValue(litVal.Val, 0) + if err != nil { + return nil, err + } + return &lang.HoverData{ + Content: lang.Markdown(content), + Range: expr.Range(), + }, nil } case *hclsyntax.LiteralValueExpr: if constraints.HasLiteralTypeOf(e.Val.Type()) { - return hoverContentForValue(e.Val, 0) + content, err := hoverContentForValue(e.Val, 0) + if err != nil { + return nil, err + } + return &lang.HoverData{ + Content: lang.Markdown(content), + Range: expr.Range(), + }, nil } lv, ok := constraints.LiteralValueOf(e.Val) if ok { - return hoverContentForValue(lv.Val, 0) + content, err := hoverContentForValue(lv.Val, 0) + if err != nil { + return nil, err + } + return &lang.HoverData{ + Content: lang.Markdown(content), + Range: expr.Range(), + }, nil } - return "", &ConstraintMismatch{e} + return nil, &ConstraintMismatch{e} + } + + return nil, fmt.Errorf("unsupported expression (%T)", expr) +} + +func hoverDataForObjectExpr(objExpr *hclsyntax.ObjectConsExpr, oe schema.ObjectExpr, pos hcl.Pos) (*lang.HoverData, error) { + for _, item := range objExpr.Items { + key, _ := item.KeyExpr.Value(nil) + if !key.IsWhollyKnown() || key.Type() != cty.String { + continue + } + attr, ok := oe.Attributes[key.AsString()] + if !ok { + // unknown attribute + continue + } + + if item.ValueExpr.Range().ContainsPos(pos) { + return hoverDataForExpr(item.ValueExpr, ExprConstraints(attr.Expr), pos) + } + + itemRng := hcl.RangeBetween(item.KeyExpr.Range(), item.ValueExpr.Range()) + if itemRng.ContainsPos(pos) { + content := fmt.Sprintf(`**%s** _%s_`, key.AsString(), attr.FriendlyName()) + if attr.Description.Value != "" { + content += fmt.Sprintf("\n\n%s", attr.Description.Value) + } + + return &lang.HoverData{ + Content: lang.Markdown(content), + Range: itemRng, + }, nil + } + } + + if len(oe.Attributes) == 0 { + content := fmt.Sprintf("_%s_", oe.FriendlyName()) + if oe.Description.Value != "" { + content += "\n\n" + oe.Description.Value + } + return &lang.HoverData{ + Content: lang.Markdown(content), + Range: objExpr.Range(), + }, nil + } + + attrNames := sortedObjectExprAttrNames(oe.Attributes) + content := "```\n{\n" + for _, name := range attrNames { + content += fmt.Sprintf(" %s = %s\n", name, oe.Attributes[name].FriendlyName()) + } + content += fmt.Sprintf("}\n```\n_%s_", oe.FriendlyName()) + if oe.Description.Value != "" { + content += "\n\n" + oe.Description.Value + } + return &lang.HoverData{ + Content: lang.Markdown(content), + Range: objExpr.Range(), + }, nil +} + +func sortedObjectExprAttrNames(attributes schema.ObjectExprAttributes) []string { + if len(attributes) == 0 { + return []string{} + } + + constraints := attributes + names := make([]string, len(constraints)) + i := 0 + for name := range constraints { + names[i] = name + i++ } - return "", fmt.Errorf("unsupported expression (%T)", expr) + sort.Strings(names) + return names } func isMultilineStringLiteral(tplExpr *hclsyntax.TemplateExpr) bool { diff --git a/decoder/hover_expressions_test.go b/decoder/hover_expressions_test.go index 198d4a9a..50ea8382 100644 --- a/decoder/hover_expressions_test.go +++ b/decoder/hover_expressions_test.go @@ -253,6 +253,165 @@ _object_`), }, nil, }, + { + "object as expression", + map[string]*schema.AttributeSchema{ + "obj": {Expr: schema.ExprConstraints{ + schema.ObjectExpr{ + Attributes: schema.ObjectExprAttributes{ + "source": schema.ObjectAttribute{ + Expr: schema.LiteralTypeOnly(cty.String), + }, + "bool": schema.ObjectAttribute{ + Expr: schema.LiteralTypeOnly(cty.Bool), + }, + "notbool": schema.ObjectAttribute{ + Expr: schema.LiteralTypeOnly(cty.String), + }, + "nested_map": schema.ObjectAttribute{ + Expr: schema.LiteralTypeOnly(cty.Map(cty.String)), + }, + "nested_obj": schema.ObjectAttribute{ + Expr: schema.LiteralTypeOnly(cty.Object(map[string]cty.Type{})), + }, + }, + }, + }}, + }, + `obj = { + source = "blah" + different = 42 + bool = true + notbool = "test" +}`, + hcl.Pos{Line: 1, Column: 3, Byte: 2}, + &lang.HoverData{ + Content: lang.Markdown(`**obj** _object_`), + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 1, + Column: 1, + Byte: 0, + }, + End: hcl.Pos{ + Line: 6, + Column: 2, + Byte: 85, + }, + }, + }, + nil, + }, + { + "object as expression - expression", + map[string]*schema.AttributeSchema{ + "obj": {Expr: schema.ExprConstraints{ + schema.ObjectExpr{ + Attributes: schema.ObjectExprAttributes{ + "source": schema.ObjectAttribute{ + Expr: schema.LiteralTypeOnly(cty.String), + }, + "bool": schema.ObjectAttribute{ + Expr: schema.LiteralTypeOnly(cty.Bool), + }, + "notbool": schema.ObjectAttribute{ + Expr: schema.LiteralTypeOnly(cty.String), + }, + "nested_map": schema.ObjectAttribute{ + Expr: schema.LiteralTypeOnly(cty.Map(cty.String)), + }, + "nested_obj": schema.ObjectAttribute{ + Expr: schema.LiteralTypeOnly(cty.Object(map[string]cty.Type{})), + }, + }, + }, + }}, + }, + `obj = { + source = "blah" + different = 42 + bool = true + notbool = "test" +}`, + hcl.Pos{Line: 2, Column: 2, Byte: 9}, + &lang.HoverData{ + Content: lang.Markdown("```" + ` +{ + bool = bool + nested_map = map of string + nested_obj = object + notbool = string + source = string +} +` + "```" + ` +_object_`), + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 1, + Column: 7, + Byte: 6, + }, + End: hcl.Pos{ + Line: 6, + Column: 2, + Byte: 85, + }, + }, + }, + nil, + }, + { + "object as expression - attribute", + map[string]*schema.AttributeSchema{ + "obj": {Expr: schema.ExprConstraints{ + schema.ObjectExpr{ + Attributes: schema.ObjectExprAttributes{ + "source": schema.ObjectAttribute{ + Expr: schema.LiteralTypeOnly(cty.String), + }, + "bool": schema.ObjectAttribute{ + Expr: schema.LiteralTypeOnly(cty.Bool), + }, + "notbool": schema.ObjectAttribute{ + Expr: schema.LiteralTypeOnly(cty.String), + }, + "nested_map": schema.ObjectAttribute{ + Expr: schema.LiteralTypeOnly(cty.Map(cty.String)), + }, + "nested_obj": schema.ObjectAttribute{ + Expr: schema.LiteralTypeOnly(cty.Object(map[string]cty.Type{})), + }, + }, + }, + }}, + }, + `obj = { + source = "blah" + different = 42 + bool = true + notbool = "test" +}`, + hcl.Pos{Line: 2, Column: 8, Byte: 15}, + &lang.HoverData{ + Content: lang.Markdown(`**source** _string_`), + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 2, + Column: 5, + Byte: 12, + }, + End: hcl.Pos{ + Line: 2, + Column: 20, + Byte: 27, + }, + }, + }, + nil, + }, { "map as type", map[string]*schema.AttributeSchema{ diff --git a/decoder/hover_test.go b/decoder/hover_test.go index b6a11f87..7bd63462 100644 --- a/decoder/hover_test.go +++ b/decoder/hover_test.go @@ -280,7 +280,11 @@ func TestDecoder_HoverAtPos_basic(t *testing.T) { Body: &schema.BodySchema{ Attributes: map[string]*schema.AttributeSchema{ "num_attr": {Expr: schema.LiteralTypeOnly(cty.Number)}, - "str_attr": {Expr: schema.LiteralTypeOnly(cty.String), Description: lang.PlainText("Special attribute")}, + "str_attr": { + Expr: schema.LiteralTypeOnly(cty.String), + IsOptional: true, + Description: lang.PlainText("Special attribute"), + }, }, }, DependentBody: map[schema.SchemaKey]*schema.BodySchema{ diff --git a/decoder/semantic_tokens.go b/decoder/semantic_tokens.go index f1c211a9..d8e78b24 100644 --- a/decoder/semantic_tokens.go +++ b/decoder/semantic_tokens.go @@ -174,6 +174,29 @@ func tokensForExpression(expr hclsyntax.Expression, constraints ExprConstraints) return tokensForTupleConsExpr(eType, litVal.Val.Type()) } case *hclsyntax.ObjectConsExpr: + oe, ok := constraints.ObjectExpr() + if ok { + for _, item := range eType.Items { + key, _ := item.KeyExpr.Value(nil) + if !key.IsWhollyKnown() || key.Type() != cty.String { + continue + } + attr, ok := oe.Attributes[key.AsString()] + if !ok { + continue + } + + tokens = append(tokens, lang.SemanticToken{ + Type: lang.TokenObjectKey, + Modifiers: []lang.SemanticTokenModifier{}, + Range: item.KeyExpr.Range(), + }) + + ec := ExprConstraints(attr.Expr) + tokens = append(tokens, tokensForExpression(item.ValueExpr, ec)...) + } + return tokens + } me, ok := constraints.MapExpr() if ok { for _, item := range eType.Items { diff --git a/decoder/semantic_tokens_expr_test.go b/decoder/semantic_tokens_expr_test.go index 3fa88ba0..dea13b6f 100644 --- a/decoder/semantic_tokens_expr_test.go +++ b/decoder/semantic_tokens_expr_test.go @@ -304,6 +304,79 @@ EOT `obj = { knownkey = 42 unknownkey = "boo" +}`, + []lang.SemanticToken{ + { // obj + Type: lang.TokenAttrName, + Modifiers: []lang.SemanticTokenModifier{}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 1, + Column: 1, + Byte: 0, + }, + End: hcl.Pos{ + Line: 1, + Column: 4, + Byte: 3, + }, + }, + }, + { // knownkey + Type: lang.TokenObjectKey, + Modifiers: []lang.SemanticTokenModifier{}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 2, + Column: 3, + Byte: 10, + }, + End: hcl.Pos{ + Line: 2, + Column: 11, + Byte: 18, + }, + }, + }, + { // 42 + Type: lang.TokenNumber, + Modifiers: []lang.SemanticTokenModifier{}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 2, + Column: 14, + Byte: 21, + }, + End: hcl.Pos{ + Line: 2, + Column: 16, + Byte: 23, + }, + }, + }, + }, + }, + { + "object as expression", + map[string]*schema.AttributeSchema{ + "obj": { + Expr: schema.ExprConstraints{ + schema.ObjectExpr{ + Attributes: schema.ObjectExprAttributes{ + "knownkey": { + Expr: schema.LiteralTypeOnly(cty.Number), + }, + }, + }, + }, + }, + }, + `obj = { + knownkey = 42 + unknownkey = "boo" }`, []lang.SemanticToken{ { // obj diff --git a/schema/expressions.go b/schema/expressions.go index 424f9499..5ec6733b 100644 --- a/schema/expressions.go +++ b/schema/expressions.go @@ -1,12 +1,37 @@ package schema import ( + "strings" + "github.com/hashicorp/hcl-lang/lang" "github.com/zclconf/go-cty/cty" ) type ExprConstraints []ExprConstraint +func (ec ExprConstraints) FriendlyName() string { + names := make([]string, 0) + for _, constraint := range ec { + if name := constraint.FriendlyName(); name != "" && + !namesContain(names, name) { + names = append(names, name) + } + } + if len(names) > 0 { + return strings.Join(names, " or ") + } + return "" +} + +func namesContain(names []string, name string) bool { + for _, n := range names { + if n == name { + return true + } + } + return false +} + type exprConstrSigil struct{} type ExprConstraint interface { @@ -73,6 +98,42 @@ func (me MapExpr) FriendlyName() string { return me.Name } +type ObjectExpr struct { + Attributes ObjectExprAttributes + Name string + Description lang.MarkupContent +} + +func (ObjectExpr) isExprConstraintImpl() exprConstrSigil { + return exprConstrSigil{} +} + +func (oe ObjectExpr) FriendlyName() string { + if oe.Name == "" { + return "object" + } + return oe.Name +} + +type ObjectExprAttributes map[string]ObjectAttribute + +func (ObjectExprAttributes) isExprConstraintImpl() exprConstrSigil { + return exprConstrSigil{} +} + +func (oe ObjectExprAttributes) FriendlyName() string { + return "attributes" +} + +type ObjectAttribute struct { + Expr ExprConstraints + Description lang.MarkupContent +} + +func (oa ObjectAttribute) FriendlyName() string { + return oa.Expr.FriendlyName() +} + type KeywordExpr struct { Keyword string Name string