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
66 changes: 65 additions & 1 deletion plugin/internal/plugin2host/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package plugin2host

import (
"context"
stdjson "encoding/json"
"errors"
"fmt"
"os"
Expand Down Expand Up @@ -205,6 +206,69 @@ func (w *nativeWalker) Exit(node hclsyntax.Node) hcl.Diagnostics {
return nil
}

// extractJSONKeys extracts attribute names from JSON bytes using encoding/json.
// This works for both object-based JSON {"foo": ...} and array-based JSON [{"foo": ...}].
func extractJSONKeys(bytes []byte) ([]string, error) {
// Try to unmarshal as an object first
var obj map[string]any
if err := stdjson.Unmarshal(bytes, &obj); err == nil {
keys := make([]string, 0, len(obj))
for k := range obj {
keys = append(keys, k)
}
return keys, nil
}

// Try as an array of objects
var arr []map[string]any
if err := stdjson.Unmarshal(bytes, &arr); err != nil {
return nil, err
}

// Collect all unique keys from all objects in the array
keysMap := make(map[string]bool)
for _, obj := range arr {
for k := range obj {
keysMap[k] = true
}
}

keys := make([]string, 0, len(keysMap))
for k := range keysMap {
keys = append(keys, k)
}
return keys, nil
}

// getJSONAttributes gets all attributes from a JSON body, supporting both object
// and array-based syntax. For array-based JSON like [{"import": {...}}], it
// extracts attribute names using encoding/json and builds a schema to extract them.
func getJSONAttributes(body hcl.Body, bytes []byte) (hcl.Attributes, hcl.Diagnostics) {
// First, try JustAttributes (works for object-based JSON)
attrs, diags := body.JustAttributes()
if !diags.HasErrors() {
return attrs, nil
}

// Extract keys using encoding/json
keys, err := extractJSONKeys(bytes)
if err != nil {
return attrs, diags // Return original JustAttributes error
}

// Build a schema with all discovered keys
schema := &hcl.BodySchema{
Attributes: make([]hcl.AttributeSchema, len(keys)),
}
for i, key := range keys {
schema.Attributes[i] = hcl.AttributeSchema{Name: key}
}

// Use PartialContent to get proper *json.expression objects
content, _, partialDiags := body.PartialContent(schema)
return content.Attributes, partialDiags
}

// WalkExpressions traverses expressions in all files by the passed walker.
// Note that it behaves differently in native HCL syntax and JSON syntax.
//
Expand Down Expand Up @@ -236,7 +300,7 @@ func (c *GRPCClient) WalkExpressions(walker tflint.ExprWalker) hcl.Diagnostics {
}

// In JSON syntax, everything can be walked as an attribute.
attrs, jsonDiags := file.Body.JustAttributes()
attrs, jsonDiags := getJSONAttributes(file.Body, file.Bytes)
if jsonDiags.HasErrors() {
diags = diags.Extend(jsonDiags)
continue
Expand Down
25 changes: 25 additions & 0 deletions plugin/internal/plugin2host/plugin2host_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1208,6 +1208,31 @@ data "terraform_remote_state" "remote_state" {
{Start: hcl.Pos{Line: 8, Column: 17}, End: hcl.Pos{Line: 8, Column: 32}},
},
},
{
name: "array-based json",
files: map[string][]byte{
"main.tf.json": []byte(`[
{
"resource": {
"null_resource": {
"foo": {}
}
}
},
{
"variable": {
"example": {
"type": "string"
}
}
}
]`),
},
walked: []hcl.Range{
{Start: hcl.Pos{Line: 3, Column: 17}, End: hcl.Pos{Line: 7, Column: 6}},
{Start: hcl.Pos{Line: 10, Column: 17}, End: hcl.Pos{Line: 14, Column: 6}},
},
},
}

for _, test := range tests {
Expand Down