diff --git a/hcl/hclsyntax/structure.go b/hcl/hclsyntax/structure.go index d69f65b..089311d 100644 --- a/hcl/hclsyntax/structure.go +++ b/hcl/hclsyntax/structure.go @@ -9,6 +9,10 @@ import ( // AsHCLBlock returns the block data expressed as a *hcl.Block. func (b *Block) AsHCLBlock() *hcl.Block { + if b == nil { + return nil + } + lastHeaderRange := b.TypeRange if len(b.LabelRanges) > 0 { lastHeaderRange = b.LabelRanges[len(b.LabelRanges)-1] @@ -326,6 +330,9 @@ func (a *Attribute) Range() hcl.Range { // AsHCLAttribute returns the block data expressed as a *hcl.Attribute. func (a *Attribute) AsHCLAttribute() *hcl.Attribute { + if a == nil { + return nil + } return &hcl.Attribute{ Name: a.Name, Expr: a.Expr, diff --git a/hcl/hclsyntax/structure_at_pos.go b/hcl/hclsyntax/structure_at_pos.go new file mode 100644 index 0000000..d8f023b --- /dev/null +++ b/hcl/hclsyntax/structure_at_pos.go @@ -0,0 +1,118 @@ +package hclsyntax + +import ( + "github.com/hashicorp/hcl2/hcl" +) + +// ----------------------------------------------------------------------------- +// The methods in this file are all optional extension methods that serve to +// implement the methods of the same name on *hcl.File when its root body +// is provided by this package. +// ----------------------------------------------------------------------------- + +// BlocksAtPos implements the method of the same name for an *hcl.File that +// is backed by a *Body. +func (b *Body) BlocksAtPos(pos hcl.Pos) []*hcl.Block { + list, _ := b.blocksAtPos(pos, true) + return list +} + +// InnermostBlockAtPos implements the method of the same name for an *hcl.File +// that is backed by a *Body. +func (b *Body) InnermostBlockAtPos(pos hcl.Pos) *hcl.Block { + _, innermost := b.blocksAtPos(pos, false) + return innermost.AsHCLBlock() +} + +// OutermostBlockAtPos implements the method of the same name for an *hcl.File +// that is backed by a *Body. +func (b *Body) OutermostBlockAtPos(pos hcl.Pos) *hcl.Block { + return b.outermostBlockAtPos(pos).AsHCLBlock() +} + +// blocksAtPos is the internal engine of both BlocksAtPos and +// InnermostBlockAtPos, which both need to do the same logic but return a +// differently-shaped result. +// +// list is nil if makeList is false, avoiding an allocation. Innermost is +// always set, and if the returned list is non-nil it will always match the +// final element from that list. +func (b *Body) blocksAtPos(pos hcl.Pos, makeList bool) (list []*hcl.Block, innermost *Block) { + current := b + +Blocks: + for current != nil { + for _, block := range current.Blocks { + wholeRange := hcl.RangeBetween(block.TypeRange, block.CloseBraceRange) + if wholeRange.ContainsPos(pos) { + innermost = block + if makeList { + list = append(list, innermost.AsHCLBlock()) + } + current = block.Body + continue Blocks + } + } + + // If we fall out here then none of the current body's nested blocks + // contain the position we are looking for, and so we're done. + break + } + + return +} + +// outermostBlockAtPos is the internal version of OutermostBlockAtPos that +// returns a hclsyntax.Block rather than an hcl.Block, allowing for further +// analysis if necessary. +func (b *Body) outermostBlockAtPos(pos hcl.Pos) *Block { + // This is similar to blocksAtPos, but simpler because we know it only + // ever needs to search the first level of nested blocks. + + for _, block := range b.Blocks { + wholeRange := hcl.RangeBetween(block.TypeRange, block.CloseBraceRange) + if wholeRange.ContainsPos(pos) { + return block + } + } + + return nil +} + +// AttributeAtPos implements the method of the same name for an *hcl.File +// that is backed by a *Body. +func (b *Body) AttributeAtPos(pos hcl.Pos) *hcl.Attribute { + return b.attributeAtPos(pos).AsHCLAttribute() +} + +// attributeAtPos is the internal version of AttributeAtPos that returns a +// hclsyntax.Block rather than an hcl.Block, allowing for further analysis if +// necessary. +func (b *Body) attributeAtPos(pos hcl.Pos) *Attribute { + searchBody := b + _, block := b.blocksAtPos(pos, false) + if block != nil { + searchBody = block.Body + } + + for _, attr := range searchBody.Attributes { + if attr.SrcRange.ContainsPos(pos) { + return attr + } + } + + return nil +} + +// OutermostExprAtPos implements the method of the same name for an *hcl.File +// that is backed by a *Body. +func (b *Body) OutermostExprAtPos(pos hcl.Pos) hcl.Expression { + attr := b.attributeAtPos(pos) + if attr == nil { + return nil + } + if !attr.Expr.Range().ContainsPos(pos) { + return nil + } + return attr.Expr +} diff --git a/hcl/hclsyntax/structure_at_pos_test.go b/hcl/hclsyntax/structure_at_pos_test.go new file mode 100644 index 0000000..2d8d910 --- /dev/null +++ b/hcl/hclsyntax/structure_at_pos_test.go @@ -0,0 +1,335 @@ +package hclsyntax + +import ( + "reflect" + "testing" + + "github.com/hashicorp/hcl2/hcl" +) + +func TestBlocksAtPos(t *testing.T) { + tests := map[string]struct { + Src string + Pos hcl.Pos + WantTypes []string + }{ + "empty": { + ``, + hcl.Pos{Byte: 0}, + nil, + }, + "spaces": { + ` `, + hcl.Pos{Byte: 1}, + nil, + }, + "single in header": { + `foo {}`, + hcl.Pos{Byte: 1}, + []string{"foo"}, + }, + "single in body": { + `foo { }`, + hcl.Pos{Byte: 7}, + []string{"foo"}, + }, + "single in body with unselected nested": { + ` + foo { + + bar { + + } + } + `, + hcl.Pos{Byte: 10}, + []string{"foo"}, + }, + "single in body with unselected sibling": { + ` + foo { } + bar { } + `, + hcl.Pos{Byte: 10}, + []string{"foo"}, + }, + "selected nested two levels": { + ` + foo { + bar { + + } + } + `, + hcl.Pos{Byte: 20}, + []string{"foo", "bar"}, + }, + "selected nested three levels": { + ` + foo { + bar { + baz { + + } + } + } + `, + hcl.Pos{Byte: 31}, + []string{"foo", "bar", "baz"}, + }, + "selected nested three levels with unselected sibling after": { + ` + foo { + bar { + baz { + + } + } + not_wanted {} + } + `, + hcl.Pos{Byte: 31}, + []string{"foo", "bar", "baz"}, + }, + "selected nested three levels with unselected sibling before": { + ` + foo { + not_wanted {} + bar { + baz { + + } + } + } + `, + hcl.Pos{Byte: 49}, + []string{"foo", "bar", "baz"}, + }, + "unterminated": { + `foo { `, + hcl.Pos{Byte: 7}, + []string{"foo"}, + }, + "unterminated nested": { + ` + foo { + bar { + } + `, + hcl.Pos{Byte: 16}, + []string{"foo", "bar"}, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + f, diags := ParseConfig([]byte(test.Src), "", hcl.Pos{Line: 1, Column: 1}) + for _, diag := range diags { + // We intentionally ignore diagnostics here because we should be + // able to work with the incomplete configuration that results + // when the parser does its recovery behavior. However, we do + // log them in case it's helpful to someone debugging a failing + // test. + t.Logf(diag.Error()) + } + + blocks := f.BlocksAtPos(test.Pos) + outermost := f.OutermostBlockAtPos(test.Pos) + innermost := f.InnermostBlockAtPos(test.Pos) + + gotTypes := make([]string, len(blocks)) + for i, block := range blocks { + gotTypes[i] = block.Type + } + + if len(test.WantTypes) == 0 { + if len(gotTypes) != 0 { + t.Errorf("wrong block types\ngot: %#v\nwant: (none)", gotTypes) + } + if outermost != nil { + t.Errorf("wrong outermost type\ngot: %#v\nwant: (none)", outermost.Type) + } + if innermost != nil { + t.Errorf("wrong innermost type\ngot: %#v\nwant: (none)", innermost.Type) + } + return + } + + if !reflect.DeepEqual(gotTypes, test.WantTypes) { + if len(gotTypes) != 0 { + t.Errorf("wrong block types\ngot: %#v\nwant: %#v", gotTypes, test.WantTypes) + } + } + if got, want := outermost.Type, test.WantTypes[0]; got != want { + t.Errorf("wrong outermost type\ngot: %#v\nwant: %#v", got, want) + } + if got, want := innermost.Type, test.WantTypes[len(test.WantTypes)-1]; got != want { + t.Errorf("wrong innermost type\ngot: %#v\nwant: %#v", got, want) + } + }) + } +} + +func TestAttributeAtPos(t *testing.T) { + tests := map[string]struct { + Src string + Pos hcl.Pos + WantName string + }{ + "empty": { + ``, + hcl.Pos{Byte: 0}, + "", + }, + "top-level": { + `foo = 1`, + hcl.Pos{Byte: 0}, + "foo", + }, + "top-level with ignored sibling after": { + ` + foo = 1 + bar = 2 + `, + hcl.Pos{Byte: 6}, + "foo", + }, + "top-level ignored sibling before": { + ` + foo = 1 + bar = 2 + `, + hcl.Pos{Byte: 17}, + "bar", + }, + "nested": { + ` + foo { + bar = 2 + } + `, + hcl.Pos{Byte: 17}, + "bar", + }, + "nested in unterminated block": { + ` + foo { + bar = 2 + `, + hcl.Pos{Byte: 17}, + "bar", + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + f, diags := ParseConfig([]byte(test.Src), "", hcl.Pos{Line: 1, Column: 1}) + for _, diag := range diags { + // We intentionally ignore diagnostics here because we should be + // able to work with the incomplete configuration that results + // when the parser does its recovery behavior. However, we do + // log them in case it's helpful to someone debugging a failing + // test. + t.Logf(diag.Error()) + } + + got := f.AttributeAtPos(test.Pos) + + if test.WantName == "" { + if got != nil { + t.Errorf("wrong attribute name\ngot: %#v\nwant: (none)", got.Name) + } + return + } + + if got == nil { + t.Fatalf("wrong attribute name\ngot: (none)\nwant: %#v", test.WantName) + } + + if got.Name != test.WantName { + t.Errorf("wrong attribute name\ngot: %#v\nwant: %#v", got.Name, test.WantName) + } + }) + } +} + +func TestOutermostExprAtPos(t *testing.T) { + tests := map[string]struct { + Src string + Pos hcl.Pos + WantSrc string + }{ + "empty": { + ``, + hcl.Pos{Byte: 0}, + ``, + }, + "simple bool": { + `a = true`, + hcl.Pos{Byte: 6}, + `true`, + }, + "simple reference": { + `a = blah`, + hcl.Pos{Byte: 6}, + `blah`, + }, + "attribute reference": { + `a = blah.foo`, + hcl.Pos{Byte: 6}, + `blah.foo`, + }, + "parens": { + `a = (1 + 1)`, + hcl.Pos{Byte: 6}, + `1 + 1`, // The parser trims the parens off, so they aren't considered as part of the expression :( + }, + "tuple cons": { + `a = [1, 2, 3]`, + hcl.Pos{Byte: 5}, + `[1, 2, 3]`, + }, + "function call": { + `a = foom("a")`, + hcl.Pos{Byte: 10}, + `foom("a")`, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + inputSrc := []byte(test.Src) + f, diags := ParseConfig(inputSrc, "", hcl.Pos{Line: 1, Column: 1}) + for _, diag := range diags { + // We intentionally ignore diagnostics here because we should be + // able to work with the incomplete configuration that results + // when the parser does its recovery behavior. However, we do + // log them in case it's helpful to someone debugging a failing + // test. + t.Logf(diag.Error()) + } + + gotExpr := f.OutermostExprAtPos(test.Pos) + var gotSrc string + if gotExpr != nil { + rng := gotExpr.Range() + gotSrc = string(rng.SliceBytes(inputSrc)) + } + + if test.WantSrc == "" { + if gotExpr != nil { + t.Errorf("wrong expression source\ngot: %s\nwant: (none)", gotSrc) + } + return + } + + if gotExpr == nil { + t.Fatalf("wrong expression source\ngot: (none)\nwant: %s", test.WantSrc) + } + + if gotSrc != test.WantSrc { + t.Errorf("wrong expression source\ngot: %#v\nwant: %#v", gotSrc, test.WantSrc) + } + }) + } +} diff --git a/hcl/pos.go b/hcl/pos.go index 1a4b329..6b7ec1d 100644 --- a/hcl/pos.go +++ b/hcl/pos.go @@ -94,6 +94,16 @@ func RangeOver(a, b Range) Range { } } +// ContainsPos returns true if and only if the given position is contained within +// the receiving range. +// +// In the unlikely case that the line/column information disagree with the byte +// offset information in the given position or receiving range, the byte +// offsets are given priority. +func (r Range) ContainsPos(pos Pos) bool { + return r.ContainsOffset(pos.Byte) +} + // ContainsOffset returns true if and only if the given byte offset is within // the receiving Range. func (r Range) ContainsOffset(offset int) bool { diff --git a/hcl/structure_at_pos.go b/hcl/structure_at_pos.go new file mode 100644 index 0000000..8521814 --- /dev/null +++ b/hcl/structure_at_pos.go @@ -0,0 +1,117 @@ +package hcl + +// ----------------------------------------------------------------------------- +// The methods in this file all have the general pattern of making a best-effort +// to find one or more constructs that contain a given source position. +// +// These all operate by delegating to an optional method of the same name and +// signature on the file's root body, allowing each syntax to potentially +// provide its own implementations of these. For syntaxes that don't implement +// them, the result is always nil. +// ----------------------------------------------------------------------------- + +// BlocksAtPos attempts to find all of the blocks that contain the given +// position, ordered so that the outermost block is first and the innermost +// block is last. This is a best-effort method that may not be able to produce +// a complete result for all positions or for all HCL syntaxes. +// +// If the returned slice is non-empty, the first element is guaranteed to +// represent the same block as would be the result of OutermostBlockAtPos and +// the last element the result of InnermostBlockAtPos. However, the +// implementation may return two different objects describing the same block, +// so comparison by pointer identity is not possible. +// +// The result is nil if no blocks at all contain the given position. +func (f *File) BlocksAtPos(pos Pos) []*Block { + // The root body of the file must implement this interface in order + // to support BlocksAtPos. + type Interface interface { + BlocksAtPos(pos Pos) []*Block + } + + impl, ok := f.Body.(Interface) + if !ok { + return nil + } + return impl.BlocksAtPos(pos) +} + +// OutermostBlockAtPos attempts to find a top-level block in the receiving file +// that contains the given position. This is a best-effort method that may not +// be able to produce a result for all positions or for all HCL syntaxes. +// +// The result is nil if no single block could be selected for any reason. +func (f *File) OutermostBlockAtPos(pos Pos) *Block { + // The root body of the file must implement this interface in order + // to support OutermostBlockAtPos. + type Interface interface { + OutermostBlockAtPos(pos Pos) *Block + } + + impl, ok := f.Body.(Interface) + if !ok { + return nil + } + return impl.OutermostBlockAtPos(pos) +} + +// InnermostBlockAtPos attempts to find the most deeply-nested block in the +// receiving file that contains the given position. This is a best-effort +// method that may not be able to produce a result for all positions or for +// all HCL syntaxes. +// +// The result is nil if no single block could be selected for any reason. +func (f *File) InnermostBlockAtPos(pos Pos) *Block { + // The root body of the file must implement this interface in order + // to support InnermostBlockAtPos. + type Interface interface { + InnermostBlockAtPos(pos Pos) *Block + } + + impl, ok := f.Body.(Interface) + if !ok { + return nil + } + return impl.InnermostBlockAtPos(pos) +} + +// OutermostExprAtPos attempts to find an expression in the receiving file +// that contains the given position. This is a best-effort method that may not +// be able to produce a result for all positions or for all HCL syntaxes. +// +// Since expressions are often nested inside one another, this method returns +// the outermost "root" expression that is not contained by any other. +// +// The result is nil if no single expression could be selected for any reason. +func (f *File) OutermostExprAtPos(pos Pos) Expression { + // The root body of the file must implement this interface in order + // to support OutermostExprAtPos. + type Interface interface { + OutermostExprAtPos(pos Pos) Expression + } + + impl, ok := f.Body.(Interface) + if !ok { + return nil + } + return impl.OutermostExprAtPos(pos) +} + +// AttributeAtPos attempts to find an attribute definition in the receiving +// file that contains the given position. This is a best-effort method that may +// not be able to produce a result for all positions or for all HCL syntaxes. +// +// The result is nil if no single attribute could be selected for any reason. +func (f *File) AttributeAtPos(pos Pos) *Attribute { + // The root body of the file must implement this interface in order + // to support OutermostExprAtPos. + type Interface interface { + AttributeAtPos(pos Pos) *Attribute + } + + impl, ok := f.Body.(Interface) + if !ok { + return nil + } + return impl.AttributeAtPos(pos) +}