Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 8 additions & 73 deletions internal/ast/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,6 @@ type IndexedAST struct {
Index NodeIndex
// Locals contains the local attributes in the file, indexed by attribute key
Locals Scope
// Includes contains the include blocks in the file, indexed by include block name
Includes Scope
}

// FindNodeAt returns the node at the given position in the file. If no node is found, returns nil.
Expand Down Expand Up @@ -111,17 +109,15 @@ func (s Scope) Add(node *IndexedNode) {
type NodeIndex map[int][]*IndexedNode

type nodeIndexBuilder struct {
index NodeIndex
locals Scope
includes Scope
stack []*IndexedNode
index NodeIndex
locals Scope
stack []*IndexedNode
}

func newNodeIndexBuilder() *nodeIndexBuilder {
return &nodeIndexBuilder{
index: make(map[int][]*IndexedNode),
locals: make(Scope),
includes: make(Scope),
index: make(map[int][]*IndexedNode),
locals: make(Scope),
}
}

Expand All @@ -141,8 +137,6 @@ func (w *nodeIndexBuilder) Enter(node hclsyntax.Node) hcl.Diagnostics {

if IsLocalAttribute(inode) {
w.locals.Add(inode)
} else if IsIncludeBlock(inode) {
w.includes.Add(inode)
}

return nil
Expand Down Expand Up @@ -176,70 +170,12 @@ func IsLocalsBlock(inode *IndexedNode) bool {
return ok && block.Type == "locals"
}

// IsIncludeBlock returns TRUE if the node is an HCL block of type "include".
func IsIncludeBlock(inode *IndexedNode) bool {
block, ok := inode.Node.(*hclsyntax.Block)
return ok && block.Type == "include" && len(block.Labels) > 0
}

// IsDependencyBlock returns TRUE if the node is an HCL block of type "dependency".
func IsDependencyBlock(inode *IndexedNode) bool {
block, ok := inode.Node.(*hclsyntax.Block)
return ok && block.Type == "dependency" && len(block.Labels) > 0
}

// IsAttribute returns TRUE if the node is an hclsyntax.Attribute.
func IsAttribute(inode *IndexedNode) bool {
_, ok := inode.Node.(*hclsyntax.Attribute)
return ok
}

// GetNodeIncludeLabel returns the label of the given node, if it is an include block.
// If the node is not an include block, returns an empty string and false.
func GetNodeIncludeLabel(inode *IndexedNode) (string, bool) {
attr := FindFirstParentMatch(inode, IsAttribute)
if attr == nil {
return "", false
}

local := FindFirstParentMatch(attr, IsIncludeBlock)
if local == nil {
return "", false
}

name := ""
if labels := local.Node.(*hclsyntax.Block).Labels; len(labels) > 0 {
name = labels[0]
}

return name, true
}

// GetNodeDependencyLabel returns whether the node is part of a dependency block's config_path field.
// If it is, returns the name of the dependency block and TRUE, otherwise returns "" and FALSE.
func GetNodeDependencyLabel(inode *IndexedNode) (string, bool) {
attr := FindFirstParentMatch(inode, IsAttribute)
if attr == nil {
return "", false
}

if attr.Node.(*hclsyntax.Attribute).Name != "config_path" {
return "", false
}

dep := FindFirstParentMatch(attr, IsDependencyBlock)
if dep == nil {
return "", false
}

name := ""
if labels := dep.Node.(*hclsyntax.Block).Labels; len(labels) > 0 {
name = labels[0]
}

return name, true
}

func FindFirstParentMatch(inode *IndexedNode, matcher func(*IndexedNode) bool) *IndexedNode {
for cur := inode; cur != nil; cur = cur.Parent {
if matcher(cur) {
Expand All @@ -258,9 +194,8 @@ func indexAST(ast *hcl.File) *IndexedAST {
_ = hclsyntax.Walk(body, builder)

return &IndexedAST{
Index: builder.index,
Locals: builder.locals,
Includes: builder.includes,
HCLFile: ast,
Index: builder.index,
Locals: builder.locals,
HCLFile: ast,
}
}
186 changes: 6 additions & 180 deletions internal/ast/ast_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,49 +216,6 @@ func TestIsLocalsBlock(t *testing.T) {
}
}

func TestIsIncludeBlock(t *testing.T) {
t.Parallel()

tc := []struct {
name string
content string
pos hcl.Pos
expected bool
}{
{
name: "not an include block",
content: `inputs = {
foo = "bar"
}`,
pos: hcl.Pos{Line: 1, Column: 1},
expected: false,
},
{
name: "include block",
content: `include "root" {
path = "root.hcl"
}`,
pos: hcl.Pos{Line: 1, Column: 1},
expected: true,
},
}

for _, tt := range tc {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

indexed, err := ast.ParseHCLFile("test.hcl", []byte(tt.content))
require.NoError(t, err)

require.NotNil(t, indexed)

node := indexed.FindNodeAt(tt.pos)

assert.Equal(t, tt.expected, ast.IsIncludeBlock(node))
})
}
}

func TestIsAttribute(t *testing.T) {
t.Parallel()

Expand Down Expand Up @@ -302,130 +259,6 @@ func TestIsAttribute(t *testing.T) {
}
}

func TestGetNodeIncludeLabel(t *testing.T) {
t.Parallel()

tc := []struct {
name string
content string
expected string
pos hcl.Pos
}{
{
name: "not an include block",
content: `inputs = {
foo = "bar"
}`,
pos: hcl.Pos{Line: 1, Column: 1},
expected: "",
},
{
name: "include block beginning of path",
content: `include "root" {
path = "root.hcl"
}`,
pos: hcl.Pos{Line: 2, Column: 2},
expected: "root",
},
{
name: "include block end of path",
content: `include "root" {
path = "root.hcl"
}`,
pos: hcl.Pos{Line: 2, Column: 18},
expected: "root",
},
}

for _, tt := range tc {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

indexed, err := ast.ParseHCLFile("test.hcl", []byte(tt.content))
require.NoError(t, err)

require.NotNil(t, indexed)

node := indexed.FindNodeAt(tt.pos)

path, ok := ast.GetNodeIncludeLabel(node)
if tt.expected == "" {
assert.False(t, ok)
return
}

assert.True(t, ok)
assert.Equal(t, tt.expected, path)
})
}
}

func TestGetNodeDependencyLabel(t *testing.T) {
t.Parallel()

tc := []struct {
name string
content string
expected string
pos hcl.Pos
}{
{
name: "not a dependency block",
content: `inputs = {
foo = "bar"
}`,
pos: hcl.Pos{Line: 1, Column: 1},
expected: "",
},
{
name: "dependency block beginning of path",
content: `dependency "vpc" {
config_path = "../vpc"
}`,
pos: hcl.Pos{Line: 2, Column: 2},
expected: "vpc",
},
{
name: "dependency block end of path",
content: `dependency "vpc" {
config_path = "../vpc"
}`,
pos: hcl.Pos{Line: 2, Column: 18},
expected: "vpc",
},
{
name: "dependency block wrong attribute",
content: `dependency "vpc" {
other_field = "../vpc"
}`,
pos: hcl.Pos{Line: 2, Column: 18},
expected: "",
},
}

for _, tt := range tc {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

indexed, err := ast.ParseHCLFile("test.hcl", []byte(tt.content))
require.NoError(t, err)

require.NotNil(t, indexed)

node := indexed.FindNodeAt(tt.pos)

path, ok := ast.GetNodeDependencyLabel(node)
if tt.expected == "" {
assert.False(t, ok)
return
}

assert.True(t, ok)
assert.Equal(t, tt.expected, path)
})
}
}

func TestFindFirstParentMatch(t *testing.T) {
t.Parallel()

Expand All @@ -450,8 +283,11 @@ func TestFindFirstParentMatch(t *testing.T) {
content: `locals {
foo = "bar"
}`,
pos: hcl.Pos{Line: 2, Column: 2},
matcher: ast.IsDependencyBlock,
pos: hcl.Pos{Line: 2, Column: 2},
matcher: func(n *ast.IndexedNode) bool {
block, ok := n.Node.(*hclsyntax.Block)
return ok && block.Type == "inputs"
},
expected: false,
},
}
Expand Down Expand Up @@ -511,7 +347,7 @@ func TestScope_Add(t *testing.T) {
})
}

// Test that include and local scopes are updated in parsing
// Test that the locals scope is populated during parsing.
func TestIndexedAST_Scopes(t *testing.T) {
t.Parallel()

Expand All @@ -520,22 +356,12 @@ locals {
region = "us-west-2"
env = "dev"
}

include "root" {
path = find_in_parent_folders()
}
`
indexed, err := ast.ParseHCLFile("test.hcl", []byte(content))
require.NoError(t, err)

// Test locals scope
locals := indexed.Locals
assert.NotNil(t, locals, "Locals scope should not be nil")
assert.Contains(t, locals, "region", "Should contain 'region' local")
assert.Contains(t, locals, "env", "Should contain 'env' local")

// Test includes scope existence
includes := indexed.Includes
assert.NotNil(t, includes, "Includes scope should not be nil")
assert.Contains(t, includes, "root", "Should contain 'root' include")
}
Loading
Loading