What problem does this solve?
HCL is parsed and intra-file interpolation refs are captured, but two things are missing that make multi-module Terraform untraceable:
- No module composition edges. A
module "x" { source = "../modules/y" } block is not linked to the module it instantiates.
- No block-type semantics. Every HCL block —
resource, data, module, variable, output, provider, locals — collapses to the generic Class label, so they are indistinguishable and unfilterable.
Evidence from an indexed repo (1,431 .tf nodes):
.tf nodes by label: Class 1,147, File/Module 142 each. Every block is Class.
- Intra-
.tf edges: USAGE 995 (interpolation attribute refs ${aws_x.y.id} — good), DEFINES 1,289 (structural), but IMPORTS = 0 and CALLS = 0.
- The
Resource label that exists in the schema is populated by the K8s extractor (e.g. Job/...), not Terraform — so there is no Terraform-specific node typing at all.
Consequences:
- "What instantiates the
vpc module?" / "what stacks consume modules/sqs_queues?" — unanswerable.
- Cannot filter by block kind (e.g. all
data sources, all aws_* resources, all variables).
- Cross-module/cross-stack blast radius is invisible despite the modules living in the same repo.
Proposed solution
In the HCL extraction path:
- Block-type labels instead of generic
Class: TFResource, TFDataSource, TFModule, TFVariable, TFOutput, TFProvider, TFLocal (or a single label + block_type property). Keep the existing qualified-name scheme (...resource.aws_lambda_function.f).
module.source -> IMPORTS edge from the module block to the resolved local module directory/Module node (relative source = "../modules/y" or "./modules/y"). External/registry sources (source = "terraform-aws-modules/vpc/aws") -> external module node, mirroring how external deps are represented elsewhere.
depends_on -> explicit edge between the two blocks (distinct from interpolation USAGE).
- Resolve
var.x / module.y.output / data.t.n references to the corresponding TFVariable / TFModule / TFDataSource nodes (extends the existing USAGE behaviour to module/var/output boundaries).
Public OSS test beds
terraform-aws-modules/terraform-aws-vpc and terraform-aws-modules/terraform-aws-eks — heavy nested-module source composition.
gruntwork-io/terragrunt-infrastructure-modules-example — multi-module/multi-stack wiring.
hashicorp/terraform-guides — mixed resource/data/module/variable/output blocks.
Alternatives considered
- Keep generic
Class + USAGE only (status quo): captures intra-file attribute refs but never module boundaries; multi-module repos look like disconnected islands.
- Treat
source as a string via search_code: grep-findable, no edge, so trace_path can't follow module composition.
- External tooling (
terraform graph): requires terraform init + provider creds and produces a separate artifact, not integrated into the code graph for cross-language queries.
Confirmations
What problem does this solve?
HCL is parsed and intra-file interpolation refs are captured, but two things are missing that make multi-module Terraform untraceable:
module "x" { source = "../modules/y" }block is not linked to the module it instantiates.resource,data,module,variable,output,provider,locals— collapses to the genericClasslabel, so they are indistinguishable and unfilterable.Evidence from an indexed repo (1,431
.tfnodes):.tfnodes by label:Class1,147,File/Module142 each. Every block isClass..tfedges:USAGE995 (interpolation attribute refs${aws_x.y.id}— good),DEFINES1,289 (structural), butIMPORTS= 0 andCALLS= 0.Resourcelabel that exists in the schema is populated by the K8s extractor (e.g.Job/...), not Terraform — so there is no Terraform-specific node typing at all.Consequences:
vpcmodule?" / "what stacks consumemodules/sqs_queues?" — unanswerable.datasources, allaws_*resources, allvariables).Proposed solution
In the HCL extraction path:
Class:TFResource,TFDataSource,TFModule,TFVariable,TFOutput,TFProvider,TFLocal(or a singlelabel+block_typeproperty). Keep the existing qualified-name scheme (...resource.aws_lambda_function.f).module.source->IMPORTSedge from themoduleblock to the resolved local module directory/Modulenode (relativesource = "../modules/y"or"./modules/y"). External/registry sources (source = "terraform-aws-modules/vpc/aws") -> external module node, mirroring how external deps are represented elsewhere.depends_on-> explicit edge between the two blocks (distinct from interpolation USAGE).var.x/module.y.output/data.t.nreferences to the correspondingTFVariable/TFModule/TFDataSourcenodes (extends the existing USAGE behaviour to module/var/output boundaries).Public OSS test beds
terraform-aws-modules/terraform-aws-vpcandterraform-aws-modules/terraform-aws-eks— heavy nested-modulesourcecomposition.gruntwork-io/terragrunt-infrastructure-modules-example— multi-module/multi-stack wiring.hashicorp/terraform-guides— mixed resource/data/module/variable/output blocks.Alternatives considered
Class+ USAGE only (status quo): captures intra-file attribute refs but never module boundaries; multi-module repos look like disconnected islands.sourceas a string via search_code: grep-findable, no edge, sotrace_pathcan't follow module composition.terraform graph): requiresterraform init+ provider creds and produces a separate artifact, not integrated into the code graph for cross-language queries.Confirmations