#3625·graphify

Terraform/HCL: attribute names and values are discarded, so attribute-level questions are unanswerable (and `query` answers them with unrelated noise)

Author: dgdelaheraCreated Sep 17, 2026Updated Sep 17, 2026

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-hcl present and working (block-level extraction is correct)