Skip to content

Commit

Permalink
Support provider-defined functions in JSON syntax
Browse files Browse the repository at this point in the history
Follow up of #214
  • Loading branch information
wata727 committed Oct 16, 2024
1 parent d6b8c4c commit 8996bb8
Show file tree
Hide file tree
Showing 2 changed files with 102 additions and 10 deletions.
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

0 comments on commit 8996bb8

Please sign in to comment.