From 0b8f6498ff8bb4cde4bb6840acd0693fc2de6712 Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Sun, 21 May 2017 17:45:31 -0700 Subject: [PATCH] hclhil: Body.Content and Body.PartialContent implementations --- zcl/hclhil/structure.go | 278 ++++++++++++++++++++++++++++++----- zcl/hclhil/structure_test.go | 200 +++++++++++++++++++++++++ 2 files changed, 445 insertions(+), 33 deletions(-) diff --git a/zcl/hclhil/structure.go b/zcl/hclhil/structure.go index 8f2b78d..1b8ca5b 100644 --- a/zcl/hclhil/structure.go +++ b/zcl/hclhil/structure.go @@ -2,6 +2,7 @@ package hclhil import ( "fmt" + "strings" "github.com/apparentlymart/go-cty/cty" "github.com/apparentlymart/go-zcl/zcl" @@ -10,15 +11,206 @@ import ( // body is our implementation of zcl.Body in terms of an HCL ObjectList type body struct { - oli *hclast.ObjectList + oli *hclast.ObjectList + hiddenNames map[string]struct{} } func (b *body) Content(schema *zcl.BodySchema) (*zcl.BodyContent, zcl.Diagnostics) { - return nil, nil + content, _, diags := b.content(schema, false) + return content, diags } func (b *body) PartialContent(schema *zcl.BodySchema) (*zcl.BodyContent, zcl.Body, zcl.Diagnostics) { - return nil, nil, nil + return b.content(schema, true) +} + +func (b *body) content(schema *zcl.BodySchema, partial bool) (*zcl.BodyContent, zcl.Body, zcl.Diagnostics) { + attrSchemas := make(map[string]zcl.AttributeSchema) + blockSchemas := make(map[string]zcl.BlockHeaderSchema) + for _, attrS := range schema.Attributes { + attrSchemas[attrS.Name] = attrS + } + for _, blockS := range schema.Blocks { + blockSchemas[blockS.Type] = blockS + } + + attrs := make(zcl.Attributes) + var blocks zcl.Blocks + var diags zcl.Diagnostics + + namesUsed := make(map[string]struct{}) + + for _, item := range b.oli.Items { + if len(item.Keys) == 0 { + // Should never happen, since we don't use b.oli.Filter + diags = append(diags, &zcl.Diagnostic{ + Severity: zcl.DiagError, + Summary: "Invalid item", + Detail: "Somehow we have an HCL item with no keys. This should never happen.", + Context: rangeFromHCLPos(item.Pos()).Ptr(), + }) + continue + } + + name := item.Keys[0].Token.Value().(string) + if _, hidden := b.hiddenNames[name]; hidden { + continue + } + + if _, isAttr := attrSchemas[name]; isAttr { + if len(item.Keys) > 1 { + diags = append(diags, &zcl.Diagnostic{ + Severity: zcl.DiagError, + Summary: "Unsupported block type", + Detail: fmt.Sprintf("Blocks of type %q are not expected here. Did you mean to define an attribute named %q?", name, name), + Context: rangeFromHCLPos(item.Pos()).Ptr(), + }) + continue + } + + diags = append(diags, insertAttr(attrs, item)...) + namesUsed[name] = struct{}{} + } else if blockS, isBlock := blockSchemas[name]; isBlock { + obj, isBlock := item.Val.(*hclast.ObjectType) + if !isBlock { + diags = append(diags, &zcl.Diagnostic{ + Severity: zcl.DiagError, + Summary: "Unsupported attribute", + Detail: fmt.Sprintf("An attribute named %q is not expected here. Did you mean to define a block of type %q?", name, name), + Context: rangeFromHCLPos(item.Pos()).Ptr(), + }) + continue + } + + if item.Assign.Line != 0 { + diags = append(diags, &zcl.Diagnostic{ + Severity: zcl.DiagWarning, + Summary: "Attribute syntax used for block", + Detail: fmt.Sprintf("Block %q is defined using attribute syntax, which is deprecated. The equals sign is not used to define a block.", name), + Context: rangeFromHCLPos(item.Pos()).Ptr(), + }) + } + + labelKeys := item.Keys[1:] + + if len(labelKeys) > len(blockS.LabelNames) { + if len(blockS.LabelNames) == 0 { + diags = append(diags, &zcl.Diagnostic{ + Severity: zcl.DiagError, + Summary: fmt.Sprintf("Extraneous label for %s", name), + Detail: fmt.Sprintf( + "No labels are expected for %s blocks", name, + ), + Context: rangeFromHCLPos(labelKeys[len(blockS.LabelNames)].Pos()).Ptr(), + }) + } else { + diags = append(diags, &zcl.Diagnostic{ + Severity: zcl.DiagError, + Summary: fmt.Sprintf("Extraneous label for %s", name), + Detail: fmt.Sprintf( + "Only %d labels (%s) are expected for %s blocks", + len(blockS.LabelNames), strings.Join(blockS.LabelNames, ", "), name, + ), + Context: rangeFromHCLPos(labelKeys[len(blockS.LabelNames)].Pos()).Ptr(), + }) + } + continue + } + if len(labelKeys) < len(blockS.LabelNames) { + diags = append(diags, &zcl.Diagnostic{ + Severity: zcl.DiagError, + Summary: fmt.Sprintf("Missing %s for %s", blockS.LabelNames[len(labelKeys)], name), + Detail: fmt.Sprintf( + "All %s blocks must have %d labels (%s).", + name, len(blockS.LabelNames), strings.Join(blockS.LabelNames, ", "), + ), + Context: rangeFromHCLPos(obj.Pos()).Ptr(), + }) + continue + } + + var labels []string + var labelRanges []zcl.Range + if len(labelKeys) > 0 { + labels = make([]string, len(labelKeys)) + labelRanges = make([]zcl.Range, len(labelKeys)) + for i, objKey := range labelKeys { + labels[i] = objKey.Token.Value().(string) + labelRanges[i] = rangeFromHCLPos(objKey.Pos()) + } + } + + blocks = append(blocks, &zcl.Block{ + Type: name, + Labels: labels, + Body: &body{ + oli: obj.List, + }, + + DefRange: rangeFromHCLPos(obj.Pos()), + TypeRange: rangeFromHCLPos(item.Keys[0].Pos()), + LabelRanges: labelRanges, + }) + namesUsed[name] = struct{}{} + + } else { + if !partial { + if item.Assign.Line == 0 { + diags = append(diags, &zcl.Diagnostic{ + Severity: zcl.DiagError, + Summary: "Unsupported block type", + Detail: fmt.Sprintf("Blocks of type %q are not expected here.", name), + Context: rangeFromHCLPos(item.Pos()).Ptr(), + }) + } else { + diags = append(diags, &zcl.Diagnostic{ + Severity: zcl.DiagError, + Summary: "Unsupported attribute", + Detail: fmt.Sprintf("An attribute named %q is not expected here.", name), + Context: rangeFromHCLPos(item.Pos()).Ptr(), + }) + } + } + } + } + + for _, attrS := range schema.Attributes { + if !attrS.Required { + continue + } + + if attrs[attrS.Name] == nil { + // HCL has a bug where it panics if you ask for the position of an + // empty object list. This means we can't specify a subject for + // this diagnostic in that case. + var subject *zcl.Range + if len(b.oli.Items) > 0 { + subject = rangeFromHCLPos(b.oli.Pos()).Ptr() + } + diags = diags.Append(&zcl.Diagnostic{ + Severity: zcl.DiagError, + Summary: "Missing required attribute", + Detail: fmt.Sprintf("The attribute %q is required, but no definition was found.", attrS.Name), + Subject: subject, + }) + } + } + + var leftovers zcl.Body + if partial { + for name := range b.hiddenNames { + namesUsed[name] = struct{}{} + } + leftovers = &body{ + oli: b.oli, + hiddenNames: namesUsed, + } + } + + return &zcl.BodyContent{ + Attributes: attrs, + Blocks: blocks, + }, leftovers, diags } func (b *body) JustAttributes() (zcl.Attributes, zcl.Diagnostics) { @@ -37,6 +229,12 @@ func (b *body) JustAttributes() (zcl.Attributes, zcl.Diagnostics) { }) continue } + + name := item.Keys[0].Token.Value().(string) + if _, hidden := b.hiddenNames[name]; hidden { + continue + } + if len(item.Keys) > 1 { name := item.Keys[0].Token.Value().(string) diags = append(diags, &zcl.Diagnostic{ @@ -48,42 +246,56 @@ func (b *body) JustAttributes() (zcl.Attributes, zcl.Diagnostics) { continue } - name := item.Keys[0].Token.Value().(string) - - if item.Assign.Line == 0 { - diags = append(diags, &zcl.Diagnostic{ - Severity: zcl.DiagWarning, - Summary: "Block syntax used for attribute", - Detail: fmt.Sprintf("Attribute %q is defined using block syntax, which is deprecated. Use an equals sign after the attribute name instead.", name), - Context: rangeFromHCLPos(item.Pos()).Ptr(), - }) - } - - if attrs[name] != nil { - diags = append(diags, &zcl.Diagnostic{ - Severity: zcl.DiagError, - Summary: "Duplicate attribute definition", - Detail: fmt.Sprintf( - "Attribute %q was previously defined at %s", - name, attrs[name].NameRange.String(), - ), - Context: rangeFromHCLPos(item.Pos()).Ptr(), - }) - continue - } - - attrs[name] = &zcl.Attribute{ - Name: name, - Expr: &expression{src: item.Val}, - Range: rangeFromHCLPos(item.Pos()), - NameRange: rangeFromHCLPos(item.Keys[0].Pos()), - } + diags = append(diags, insertAttr(attrs, item)...) } return attrs, diags } +func insertAttr(attrs zcl.Attributes, item *hclast.ObjectItem) zcl.Diagnostics { + name := item.Keys[0].Token.Value().(string) + var diags zcl.Diagnostics + + if item.Assign.Line == 0 { + diags = append(diags, &zcl.Diagnostic{ + Severity: zcl.DiagWarning, + Summary: "Block syntax used for attribute", + Detail: fmt.Sprintf("Attribute %q is defined using block syntax, which is deprecated. Use an equals sign after the attribute name instead.", name), + Context: rangeFromHCLPos(item.Pos()).Ptr(), + }) + } + + if attrs[name] != nil { + diags = append(diags, &zcl.Diagnostic{ + Severity: zcl.DiagError, + Summary: "Duplicate attribute definition", + Detail: fmt.Sprintf( + "Attribute %q was previously defined at %s", + name, attrs[name].NameRange.String(), + ), + Context: rangeFromHCLPos(item.Pos()).Ptr(), + }) + return diags + } + + attrs[name] = &zcl.Attribute{ + Name: name, + Expr: &expression{src: item.Val}, + Range: rangeFromHCLPos(item.Pos()), + NameRange: rangeFromHCLPos(item.Keys[0].Pos()), + } + + return diags +} + func (b *body) MissingItemRange() zcl.Range { + if len(b.oli.Items) == 0 { + // Can't return a sensible range in this case, because HCL panics if + // you ask for the position of an empty list. + return zcl.Range{ + Filename: "", + } + } return rangeFromHCLPos(b.oli.Pos()) } diff --git a/zcl/hclhil/structure_test.go b/zcl/hclhil/structure_test.go index 5dac27b..05a261c 100644 --- a/zcl/hclhil/structure_test.go +++ b/zcl/hclhil/structure_test.go @@ -11,6 +11,206 @@ import ( hcltoken "github.com/hashicorp/hcl/hcl/token" ) +func TestBodyPartialContent(t *testing.T) { + tests := []struct { + Source string + Schema *zcl.BodySchema + Want *zcl.BodyContent + DiagCount int + }{ + { + ``, + &zcl.BodySchema{}, + &zcl.BodyContent{ + Attributes: zcl.Attributes{}, + }, + 0, + }, + { + `foo = 1`, + &zcl.BodySchema{}, + &zcl.BodyContent{ + Attributes: zcl.Attributes{}, + }, + 0, + }, + { + `foo = 1`, + &zcl.BodySchema{ + Attributes: []zcl.AttributeSchema{ + { + Name: "foo", + }, + }, + }, + &zcl.BodyContent{ + Attributes: zcl.Attributes{ + "foo": { + Name: "foo", + Expr: &expression{ + src: &hclast.LiteralType{ + Token: hcltoken.Token{ + Type: hcltoken.NUMBER, + Text: `1`, + Pos: hcltoken.Pos{ + Offset: 6, + Line: 1, + Column: 7, + }, + }, + }, + }, + + Range: zcl.Range{ + Start: zcl.Pos{Byte: 0, Line: 1, Column: 1}, + End: zcl.Pos{Byte: 1, Line: 1, Column: 2}, + }, + NameRange: zcl.Range{ + Start: zcl.Pos{Byte: 0, Line: 1, Column: 1}, + End: zcl.Pos{Byte: 1, Line: 1, Column: 2}, + }, + }, + }, + }, + 0, + }, + { + ``, + &zcl.BodySchema{ + Attributes: []zcl.AttributeSchema{ + { + Name: "foo", + Required: true, + }, + }, + }, + &zcl.BodyContent{ + Attributes: zcl.Attributes{}, + }, + 1, // missing required attribute + }, + { + `foo {}`, + &zcl.BodySchema{ + Blocks: []zcl.BlockHeaderSchema{ + { + Type: "foo", + }, + }, + }, + &zcl.BodyContent{ + Attributes: zcl.Attributes{}, + Blocks: zcl.Blocks{ + { + Type: "foo", + Body: &body{ + oli: &hclast.ObjectList{}, + }, + DefRange: zcl.Range{ + Start: zcl.Pos{Byte: 4, Line: 1, Column: 5}, + End: zcl.Pos{Byte: 5, Line: 1, Column: 6}, + }, + TypeRange: zcl.Range{ + Start: zcl.Pos{Byte: 0, Line: 1, Column: 1}, + End: zcl.Pos{Byte: 1, Line: 1, Column: 2}, + }, + }, + }, + }, + 0, + }, + { + `foo "unwanted" {}`, + &zcl.BodySchema{ + Blocks: []zcl.BlockHeaderSchema{ + { + Type: "foo", + }, + }, + }, + &zcl.BodyContent{ + Attributes: zcl.Attributes{}, + }, + 1, // no labels are expected + }, + { + `foo {}`, + &zcl.BodySchema{ + Blocks: []zcl.BlockHeaderSchema{ + { + Type: "foo", + LabelNames: []string{"name"}, + }, + }, + }, + &zcl.BodyContent{ + Attributes: zcl.Attributes{}, + }, + 1, // missing name + }, + { + `foo "wanted" {}`, + &zcl.BodySchema{ + Blocks: []zcl.BlockHeaderSchema{ + { + Type: "foo", + LabelNames: []string{"name"}, + }, + }, + }, + &zcl.BodyContent{ + Attributes: zcl.Attributes{}, + Blocks: zcl.Blocks{ + { + Type: "foo", + Labels: []string{"wanted"}, + Body: &body{ + oli: &hclast.ObjectList{}, + }, + DefRange: zcl.Range{ + Start: zcl.Pos{Byte: 13, Line: 1, Column: 14}, + End: zcl.Pos{Byte: 14, Line: 1, Column: 15}, + }, + TypeRange: zcl.Range{ + Start: zcl.Pos{Byte: 0, Line: 1, Column: 1}, + End: zcl.Pos{Byte: 1, Line: 1, Column: 2}, + }, + LabelRanges: []zcl.Range{ + { + Start: zcl.Pos{Byte: 4, Line: 1, Column: 5}, + End: zcl.Pos{Byte: 5, Line: 1, Column: 6}, + }, + }, + }, + }, + }, + 0, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) { + file, diags := Parse([]byte(test.Source), "test.hcl") + if len(diags) != 0 { + t.Fatalf("diagnostics from parse: %s", diags.Error()) + } + + got, _, diags := file.Body.PartialContent(test.Schema) + if len(diags) != test.DiagCount { + t.Errorf("wrong number of diagnostics %d; want %d", len(diags), test.DiagCount) + for _, diag := range diags { + t.Logf(" - %s", diag.Error()) + } + } + + if !reflect.DeepEqual(got, test.Want) { + t.Errorf("wrong result\ngot: %s\nwant: %s", spew.Sdump(got), spew.Sdump(test.Want)) + } + }) + } + +} + func TestBodyJustAttributes(t *testing.T) { tests := []struct { Source string