Skip to content

Commit ac15058

Browse files
authored
Support provider-defined functions in JSON syntax (#215)
Follow up of #214
1 parent d6b8c4c commit ac15058

File tree

2 files changed

+102
-10
lines changed

2 files changed

+102
-10
lines changed

terraform/runner.go

Lines changed: 81 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ import (
55

66
"github.com/hashicorp/hcl/v2"
77
"github.com/hashicorp/hcl/v2/hclsyntax"
8+
"github.com/hashicorp/hcl/v2/json"
89
"github.com/terraform-linters/tflint-plugin-sdk/hclext"
910
"github.com/terraform-linters/tflint-plugin-sdk/tflint"
11+
"github.com/zclconf/go-cty/cty"
1012
)
1113

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

245247
walkDiags := r.WalkExpressions(tflint.ExprWalkFunc(func(expr hcl.Expression) hcl.Diagnostics {
246-
if fce, ok := expr.(*hclsyntax.FunctionCallExpr); ok {
247-
parts := strings.Split(fce.Name, "::")
248-
if len(parts) < 2 || parts[0] != "provider" || parts[1] == "" {
248+
// For JSON syntax, walker is not implemented,
249+
// so extract the hclsyntax.Node that we can walk on.
250+
// See https://github.com/hashicorp/hcl/issues/543
251+
nodes, diags := r.walkableNodesInExpr(expr)
252+
253+
for _, node := range nodes {
254+
visitDiags := hclsyntax.VisitAll(node, func(n hclsyntax.Node) hcl.Diagnostics {
255+
if funcCallExpr, ok := n.(*hclsyntax.FunctionCallExpr); ok {
256+
parts := strings.Split(funcCallExpr.Name, "::")
257+
if len(parts) < 2 || parts[0] != "provider" || parts[1] == "" {
258+
return nil
259+
}
260+
providerRefs[parts[1]] = &ProviderRef{
261+
Name: parts[1],
262+
DefRange: funcCallExpr.Range(),
263+
}
264+
}
249265
return nil
250-
}
251-
providerRefs[parts[1]] = &ProviderRef{
252-
Name: parts[1],
253-
DefRange: expr.Range(),
254-
}
266+
})
267+
diags = diags.Extend(visitDiags)
255268
}
256-
return nil
269+
return diags
257270
}))
258271
diags = diags.Extend(walkDiags)
259272
if walkDiags.HasErrors() {
@@ -262,3 +275,62 @@ func (r *Runner) GetProviderRefs() (map[string]*ProviderRef, hcl.Diagnostics) {
262275

263276
return providerRefs, diags
264277
}
278+
279+
// walkableNodesInExpr returns hclsyntax.Node from the given expression.
280+
// If the expression is an hclsyntax expression, it is returned as is.
281+
// If the expression is a JSON expression, it is parsed and
282+
// hclsyntax.Node it contains is returned.
283+
func (r *Runner) walkableNodesInExpr(expr hcl.Expression) ([]hclsyntax.Node, hcl.Diagnostics) {
284+
nodes := []hclsyntax.Node{}
285+
286+
expr = hcl.UnwrapExpressionUntil(expr, func(expr hcl.Expression) bool {
287+
_, native := expr.(hclsyntax.Expression)
288+
return native || json.IsJSONExpression(expr)
289+
})
290+
if expr == nil {
291+
return nil, nil
292+
}
293+
294+
if json.IsJSONExpression(expr) {
295+
// HACK: For JSON expressions, we can get the JSON value as a literal
296+
// without any prior HCL parsing by evaluating it in a nil context.
297+
// We can take advantage of this property to walk through cty.Value
298+
// that may contain HCL expressions instead of walking through
299+
// expression nodes directly.
300+
// See https://github.com/hashicorp/hcl/issues/642
301+
val, diags := expr.Value(nil)
302+
if diags.HasErrors() {
303+
return nodes, diags
304+
}
305+
306+
err := cty.Walk(val, func(path cty.Path, v cty.Value) (bool, error) {
307+
if v.Type() != cty.String || v.IsNull() || !v.IsKnown() {
308+
return true, nil
309+
}
310+
311+
node, parseDiags := hclsyntax.ParseTemplate([]byte(v.AsString()), expr.Range().Filename, expr.Range().Start)
312+
if diags.HasErrors() {
313+
diags = diags.Extend(parseDiags)
314+
return true, nil
315+
}
316+
317+
nodes = append(nodes, node)
318+
return true, nil
319+
})
320+
if err != nil {
321+
return nodes, hcl.Diagnostics{{
322+
Severity: hcl.DiagError,
323+
Summary: "Failed to walk the expression value",
324+
Detail: err.Error(),
325+
Subject: expr.Range().Ptr(),
326+
}}
327+
}
328+
329+
return nodes, diags
330+
}
331+
332+
// The JSON syntax is already processed, so it's guaranteed to be native syntax.
333+
nodes = append(nodes, expr.(hclsyntax.Expression))
334+
335+
return nodes, nil
336+
}

terraform/runner_test.go

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ locals {
177177
func TestGetProviderRefs(t *testing.T) {
178178
tests := []struct {
179179
name string
180+
json bool
180181
content string
181182
want map[string]*ProviderRef
182183
}{
@@ -270,11 +271,30 @@ output "foo" {
270271
"time": {Name: "time", DefRange: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 3, Column: 11}, End: hcl.Pos{Line: 3, Column: 64}}},
271272
},
272273
},
274+
{
275+
name: "provider-defined function in JSON",
276+
json: true,
277+
content: `
278+
{
279+
"output": {
280+
"foo": {
281+
"value": "${provider::time::rfc3339_parse(\"2023-07-25T23:43:16Z\")}"
282+
}
283+
}
284+
}`,
285+
want: map[string]*ProviderRef{
286+
"time": {Name: "time", DefRange: hcl.Range{Filename: "main.tf.json", Start: hcl.Pos{Line: 3, Column: 15}, End: hcl.Pos{Line: 3, Column: 68}}},
287+
},
288+
},
273289
}
274290

275291
for _, test := range tests {
276292
t.Run(test.name, func(t *testing.T) {
277-
runner := NewRunner(helper.TestRunner(t, map[string]string{"main.tf": test.content}))
293+
filename := "main.tf"
294+
if test.json {
295+
filename += ".json"
296+
}
297+
runner := NewRunner(helper.TestRunner(t, map[string]string{filename: test.content}))
278298

279299
got, diags := runner.GetProviderRefs()
280300
if diags.HasErrors() {

0 commit comments

Comments
 (0)