Terraform/HCL: attribute names and values are discarded, so attribute-level questions are unanswerable (and `query` answers them with unrelated noise)
Summary
graphify/extractors/terraform.py creates nodes for block addresses (resource.*, data.*, module.*, var.*, output.*, provider.*, local.*) and references / depends_on edges between them. It walks every attribute node, but only to mine variable_expr descendants into reference edges — the attribute's name and literal value are discarded.
The consequence is that any attribute whose value is a literal contributes nothing at all to the graph, so questions of the form "which resources set <attr> to <value>" cannot be answered from graph.json.
Reproduction
graphify 0.9.63, graphifyy[terraform,sql].
main.tf — 8 attributes across 3 blocks:
variable "bucket_name" {
type = string
}
resource "aws_s3_bucket" "example" {
bucket = var.bucket_name
force_destroy = true
tags = {
Environment = "production"
}
}
resource "aws_db_instance" "example" {
engine_version = "15.4"
instance_class = "db.t3.micro"
allocated_storage = 20
multi_az = false
}
$ graphify .
[graphify extract] found 1 code, 0 docs, 0 papers, 0 images
[graphify extract] AST extraction on 1 code files...
[graphify extract] wrote graphify-out/graph.json: 5 nodes, 5 edges, 2 communities
Everything the graph contains:
NODE 'main.tf'
NODE 'Terraform module: .'
NODE 'aws_s3_bucket.example' L5
NODE 'aws_db_instance.example' L14
NODE 'var.bucket_name' L1
EDGE contains <dir> -> main.tf
EDGE contains main.tf -> var.bucket_name
EDGE contains main.tf -> aws_s3_bucket.example
EDGE contains main.tf -> aws_db_instance.example
EDGE references aws_s3_bucket.example -> var.bucket_name
Of the 8 attributes, exactly one leaves a trace: bucket = var.bucket_name, and only as a references edge, because its value happens to contain a var. expression. force_destroy, tags, engine_version, instance_class, allocated_storage and multi_az produce nothing.
aws_db_instance.example has four attributes and contributes no edge beyond contains — nothing about how it is configured is queryable.
Root cause
extract_terraform → _collect_refs:
def _collect_refs(node, owner_nid: str, relation: str) -> None:
rel = relation
if node.type == "attribute":
key_node = node.child_by_field_name("key") or (
node.children[0] if node.children else None
)
if key_node is not None and _read(key_node) == "depends_on":
rel = "depends_on"
if node.type == "variable_expr":
addr = _ref_address(node)
if addr:
_add_edge(owner_nid, addr, rel, node.start_point[0] + 1)
for c in node.children:
if c.is_named:
_collect_refs(c, owner_nid, rel)
The attribute branch reads key only to decide whether the relation should be depends_on. The key is otherwise unused, and the value subtree is scanned solely for variable_expr. Nested blocks (lifecycle, versioning, ingress, dynamic) have no representation either.
Why this is worse than returning nothing
Because no HCL node matches the query terms, seeding falls back to label matches elsewhere in the graph. On a large multi-language monorepo graph (~217k nodes, ~6.7k of them from .tf files, the rest mostly Java/Go/Rust/TS):
$ graphify query "which aws_s3_bucket resources set force_destroy = true"
Graph: graph.json (217041 nodes) | Traversal: BFS depth=2
Start: ['Bucket', 's3', 'RESOURCES', 'AWS', '.destroy()', 'set', 'TrueFilter'] | 5162 nodes found
All 5,162 are unrelated Java/Rust symbols — not one Terraform node. The seed list is entirely name collisions.
The failure is silent and misleading: a user reasonably concludes HCL extraction is broken, when in fact it works and merely lacks attribute granularity. An empty result, or an explicit "attributes are not represented", would be much better than confident noise.
Suggested fix
Emit attribute data under the owning block. Even the minimal version — storing {name: value_text, line} as a property on the block node — makes attribute questions answerable with almost no change to node count. A fuller version adds attribute nodes with a sets edge from the owning block, and recurses into nested blocks so resource.aws_s3_bucket.x.lifecycle.prevent_destroy is addressable.
Happy to send a PR if the approach sounds right.
Related observation
In the same workspace graph, all 15,254 edges touching a .tf node had both ends in .tf — the Terraform subgraph is entirely disconnected from the rest of the graph, so graphify path cannot relate a service's application code to its own deploy/ directory. Happy to split that into a separate issue if it is worth tracking.
Environment
- graphify 0.9.63, installed via
uv tool install "graphifyy[terraform,sql]" - macOS (darwin 24.6.0), Python 3.13
tree-sitter-hclpresent and working (block-level extraction is correct)
Source: Graphify-Labs/graphify