Skip to content
Merged
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
90 changes: 81 additions & 9 deletions terraform/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import (

"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/hashicorp/hcl/v2/json"
"github.com/terraform-linters/tflint-plugin-sdk/hclext"
"github.com/terraform-linters/tflint-plugin-sdk/tflint"
"github.com/zclconf/go-cty/cty"
)

// Runner is a custom runner that provides helper functions for this ruleset.
Expand Down Expand Up @@ -243,17 +245,28 @@ func (r *Runner) GetProviderRefs() (map[string]*ProviderRef, hcl.Diagnostics) {
}

walkDiags := r.WalkExpressions(tflint.ExprWalkFunc(func(expr hcl.Expression) hcl.Diagnostics {
if fce, ok := expr.(*hclsyntax.FunctionCallExpr); ok {
parts := strings.Split(fce.Name, "::")
if len(parts) < 2 || parts[0] != "provider" || parts[1] == "" {
// For JSON syntax, walker is not implemented,
// so extract the hclsyntax.Node that we can walk on.
// See https://github.com/hashicorp/hcl/issues/543
nodes, diags := r.walkableNodesInExpr(expr)

for _, node := range nodes {
visitDiags := hclsyntax.VisitAll(node, func(n hclsyntax.Node) hcl.Diagnostics {
if funcCallExpr, ok := n.(*hclsyntax.FunctionCallExpr); ok {
parts := strings.Split(funcCallExpr.Name, "::")
if len(parts) < 2 || parts[0] != "provider" || parts[1] == "" {
return nil
}
providerRefs[parts[1]] = &ProviderRef{
Name: parts[1],
DefRange: funcCallExpr.Range(),
}
}
return nil
}
providerRefs[parts[1]] = &ProviderRef{
Name: parts[1],
DefRange: expr.Range(),
}
})
diags = diags.Extend(visitDiags)
}
return nil
return diags
}))
diags = diags.Extend(walkDiags)
if walkDiags.HasErrors() {
Expand All @@ -262,3 +275,62 @@ func (r *Runner) GetProviderRefs() (map[string]*ProviderRef, hcl.Diagnostics) {

return providerRefs, diags
}

// walkableNodesInExpr returns hclsyntax.Node from the given expression.
// If the expression is an hclsyntax expression, it is returned as is.
// If the expression is a JSON expression, it is parsed and
// hclsyntax.Node it contains is returned.
func (r *Runner) walkableNodesInExpr(expr hcl.Expression) ([]hclsyntax.Node, hcl.Diagnostics) {
nodes := []hclsyntax.Node{}

expr = hcl.UnwrapExpressionUntil(expr, func(expr hcl.Expression) bool {
_, native := expr.(hclsyntax.Expression)
return native || json.IsJSONExpression(expr)
})
if expr == nil {
return nil, nil
}

if json.IsJSONExpression(expr) {
// HACK: For JSON expressions, we can get the JSON value as a literal
// without any prior HCL parsing by evaluating it in a nil context.
// We can take advantage of this property to walk through cty.Value
// that may contain HCL expressions instead of walking through
// expression nodes directly.
// See https://github.com/hashicorp/hcl/issues/642
val, diags := expr.Value(nil)
if diags.HasErrors() {
return nodes, diags
}

err := cty.Walk(val, func(path cty.Path, v cty.Value) (bool, error) {
if v.Type() != cty.String || v.IsNull() || !v.IsKnown() {
return true, nil
}

node, parseDiags := hclsyntax.ParseTemplate([]byte(v.AsString()), expr.Range().Filename, expr.Range().Start)
if diags.HasErrors() {
diags = diags.Extend(parseDiags)
return true, nil
}

nodes = append(nodes, node)
return true, nil
})
if err != nil {
return nodes, hcl.Diagnostics{{
Severity: hcl.DiagError,
Summary: "Failed to walk the expression value",
Detail: err.Error(),
Subject: expr.Range().Ptr(),
}}
}

return nodes, diags
}

// The JSON syntax is already processed, so it's guaranteed to be native syntax.
nodes = append(nodes, expr.(hclsyntax.Expression))

return nodes, nil
}
22 changes: 21 additions & 1 deletion terraform/runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ locals {
func TestGetProviderRefs(t *testing.T) {
tests := []struct {
name string
json bool
content string
want map[string]*ProviderRef
}{
Expand Down Expand Up @@ -270,11 +271,30 @@ output "foo" {
"time": {Name: "time", DefRange: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 3, Column: 11}, End: hcl.Pos{Line: 3, Column: 64}}},
},
},
{
name: "provider-defined function in JSON",
json: true,
content: `
{
"output": {
"foo": {
"value": "${provider::time::rfc3339_parse(\"2023-07-25T23:43:16Z\")}"
}
}
}`,
want: map[string]*ProviderRef{
"time": {Name: "time", DefRange: hcl.Range{Filename: "main.tf.json", Start: hcl.Pos{Line: 3, Column: 15}, End: hcl.Pos{Line: 3, Column: 68}}},
},
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
runner := NewRunner(helper.TestRunner(t, map[string]string{"main.tf": test.content}))
filename := "main.tf"
if test.json {
filename += ".json"
}
runner := NewRunner(helper.TestRunner(t, map[string]string{filename: test.content}))

got, diags := runner.GetProviderRefs()
if diags.HasErrors() {
Expand Down