Skip to content

Commit 98c500d

Browse files
anubhav-goelAnubhav Goeljpogran-hashi
authored
feat: TF-25532: Recognize tfcomponent.hcl extension (#1977)
* feat: TF-25532: Recognize tfcomponent.hcl extension * feat: TF-25532: Added: changelog * Update changelog message Co-authored-by: James Pogran <[email protected]> * feat: TF-25532: Refactored: order of stack file names * feat: TF-25532: Fixed: documentation regarding language id and supported file names * feat: TF-25532: Modified: bumped hcl-lang version * Revert "feat: TF-25532: Modified: bumped hcl-lang version" This reverts commit 914a3c9. * feat: TF-25532: Modified: bumped terraform-schema version * feat: TF-25532: Modified: bumped terraform-schema and hcl-lang version * feat: TF-25532: Modified: bumped terraform-schema version --------- Co-authored-by: Anubhav Goel <[email protected]> Co-authored-by: James Pogran <[email protected]>
1 parent 367fed2 commit 98c500d

File tree

20 files changed

+173
-47
lines changed

20 files changed

+173
-47
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
kind: ENHANCEMENTS
2+
body: 'Support new Terraform Stacks tfcomponent.hcl extension'
3+
time: 2025-06-02T10:57:45.782842+05:30
4+
custom:
5+
Issue: "1977"
6+
Repository: terraform-ls

docs/USAGE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ The following filetypes are supported by the Terraform Language Server:
88

99
- `terraform` - standard `*.tf` config files
1010
- `terraform-vars` - variable files (`*.tfvars`)
11-
- `terraform-stack` - standard `*.tfstack.hcl` files
11+
- `terraform-stack` - standard `*.tfcomponent.hcl` and `*.tfstack.hcl` files
1212
- `terraform-deploy` - standard `*.tfdeploy.hcl` files
1313

1414
_NOTE:_ Clients should be configured to follow the above language ID conventions

docs/architecture.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ We currently have several features:
5959
- `*.tf` and `*.tf.json` files are handled in the `modules` feature
6060
- `*.tfvars` and `*.tfvars.json` files are handled in the `variables` feature
6161
- `.terraform/` and `.terraform.lock.hcl` related operations are handled in the `rootmodules` feature
62-
- `*.tfstack.hcl` and `*.tfdeploy.hcl` files are handled in the `stacks` feature
62+
- `*.tfcomponent.hcl`, `*.tfstack.hcl` and `*.tfdeploy.hcl` files are handled in the `stacks` feature
6363

6464
A feature can provide data to the external consumers through methods. For example, the `variables` feature needs a list of variables from the `modules` feature. There should be no direct import from feature packages (we could enforce this by using `internal/`, but we won't for now) into other parts of the codebase. The "hot path" service mentioned above takes care of initializing each feature at the start of a new LS session.
6565

@@ -92,12 +92,12 @@ The `jobs` package of each feature contains all the different indexing jobs need
9292

9393
### Stack Feature Jobs
9494

95-
- `ParseStackConfiguration` - parses `*.tfstack.hcl` and `*.tfdeploy.hcl` files to turn `[]byte` into `hcl` types (AST)
95+
- `ParseStackConfiguration` - parses `*.tfcomponent.hcl`, `*.tfstack.hcl` and `*.tfdeploy.hcl` files to turn `[]byte` into `hcl` types (AST)
9696
- `LoadStackMetadata` - uses [`earlydecoder`](https://pkg.go.dev/github.com/hashicorp/terraform-schema@main/stacks/earlydecoder) to do early TF version-agnostic decoding to obtain metadata (variables, outputs etc.) which can be used to do more detailed decoding in hot-path within `hcl-lang` decoder
9797
- `PreloadEmbeddedSchema` – loads provider schemas based on provider requirements from the bundled schemas
98-
- `DecodeReferenceTargets` - uses `hcl-lang` decoder to collect reference targets within `*.tfstack.hcl` and `*.tfdeploy.hcl`
99-
- `DecodeReferenceOrigins` - uses `hcl-lang` decoder to collect reference origins within `*.tfstack.hcl` and `*.tfdeploy.hcl`
100-
- `SchemaStackValidation` - does schema-based validation of module files (`*.tfstack.hcl` and `*.tfdeploy.hcl`) and produces diagnostics associated with any "invalid" parts of code
98+
- `DecodeReferenceTargets` - uses `hcl-lang` decoder to collect reference targets within `*.tfcomponent.hcl`, `*.tfstack.hcl` and `*.tfdeploy.hcl`
99+
- `DecodeReferenceOrigins` - uses `hcl-lang` decoder to collect reference origins within `*.tfcomponent.hcl`, `*.tfstack.hcl` and `*.tfdeploy.hcl`
100+
- `SchemaStackValidation` - does schema-based validation of module files (`*.tfcomponent.hcl`, `*.tfstack.hcl` and `*.tfdeploy.hcl`) and produces diagnostics associated with any "invalid" parts of code
101101
- `ReferenceValidation` - does validation based on (mis)matched reference origins and targets, to flag up "orphaned" references
102102

103103
### Adding a new feature / "language"

docs/language-clients.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ The following file types are currently supported and language IDs expected:
88

99
- `terraform` - standard `*.tf` config files
1010
- `terraform-vars` - variable files (`*.tfvars`)
11-
- `terraform-stack` - standard `*.tfstack.hcl` files
12-
- `terraform-deploy` - standard `*.tfstack.hcl` files
11+
- `terraform-stack` - standard `*.tfcomponent.hcl` and `*.tfstack.hcl` files
12+
- `terraform-deploy` - standard `*.tfdeploy.hcl` files
1313

1414
Client can choose to highlight other files locally, but such other files
1515
must **not** be send to the server as the server isn't equipped to handle those.
@@ -46,7 +46,7 @@ This allows IntelliSense to remain accurate e.g. when switching branches in VCS
4646
or when there are any other changes made to these files outside the editor.
4747

4848
If the client implements file watcher, it should watch for any changes
49-
in `**/*.tf`, `**/*.tfvars`, `**/*.tfstack.hcl` and `**/*.tfstack.hcl` files in the workspace.
49+
in `**/*.tf`, `**/*.tfvars`, `*.tfstack.hcl`, `**/*.tfstack.hcl` and `**/*.tfstack.hcl` files in the workspace.
5050

5151
Client should **not** send changes for any other files.
5252

go.mod

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@ require (
1414
github.com/hashicorp/go-uuid v1.0.3
1515
github.com/hashicorp/go-version v1.7.0
1616
github.com/hashicorp/hc-install v0.9.2
17-
github.com/hashicorp/hcl-lang v0.0.0-20250117153936-66cdc97e9d3b
17+
github.com/hashicorp/hcl-lang v0.0.0-20250613065305-ef4e1a57cead
1818
github.com/hashicorp/hcl/v2 v2.23.0
1919
github.com/hashicorp/terraform-exec v0.23.0
2020
github.com/hashicorp/terraform-json v0.25.0
21-
github.com/hashicorp/terraform-registry-address v0.2.5
22-
github.com/hashicorp/terraform-schema v0.0.0-20250117153811-3c4991466f2c
21+
github.com/hashicorp/terraform-registry-address v0.3.0
22+
github.com/hashicorp/terraform-schema v0.0.0-20250616115602-34f2164294a0
2323
github.com/mcuadros/go-defaults v1.2.0
2424
github.com/mh-cbon/go-fmt-fail v0.0.0-20160815164508-67765b3fbcb5
2525
github.com/mitchellh/cli v1.1.5

go.sum

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -116,18 +116,20 @@ github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+l
116116
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
117117
github.com/hashicorp/hc-install v0.9.2 h1:v80EtNX4fCVHqzL9Lg/2xkp62bbvQMnvPQ0G+OmtO24=
118118
github.com/hashicorp/hc-install v0.9.2/go.mod h1:XUqBQNnuT4RsxoxiM9ZaUk0NX8hi2h+Lb6/c0OZnC/I=
119-
github.com/hashicorp/hcl-lang v0.0.0-20250117153936-66cdc97e9d3b h1:JWLbh10Hji/SYrBGwaWmvmqvbbOxQzuFZ0CplYCwCM4=
120-
github.com/hashicorp/hcl-lang v0.0.0-20250117153936-66cdc97e9d3b/go.mod h1:7aFvdIfHocBadjQ6j5RbxV0rSEasCPj0RTj/ujGCmi8=
119+
github.com/hashicorp/hcl-lang v0.0.0-20250613065305-ef4e1a57cead h1:Nthdz3YLW98zGxDKgkBj6lTXWKhp1tWDg8ecyLkYe94=
120+
github.com/hashicorp/hcl-lang v0.0.0-20250613065305-ef4e1a57cead/go.mod h1:lUY+oHKrbuViZqM+HHqmKE/18umUBPLAIoVT0WSZjKE=
121121
github.com/hashicorp/hcl/v2 v2.23.0 h1:Fphj1/gCylPxHutVSEOf2fBOh1VE4AuLV7+kbJf3qos=
122122
github.com/hashicorp/hcl/v2 v2.23.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA=
123123
github.com/hashicorp/terraform-exec v0.23.0 h1:MUiBM1s0CNlRFsCLJuM5wXZrzA3MnPYEsiXmzATMW/I=
124124
github.com/hashicorp/terraform-exec v0.23.0/go.mod h1:mA+qnx1R8eePycfwKkCRk3Wy65mwInvlpAeOwmA7vlY=
125125
github.com/hashicorp/terraform-json v0.25.0 h1:rmNqc/CIfcWawGiwXmRuiXJKEiJu1ntGoxseG1hLhoQ=
126126
github.com/hashicorp/terraform-json v0.25.0/go.mod h1:sMKS8fiRDX4rVlR6EJUMudg1WcanxCMoWwTLkgZP/vc=
127-
github.com/hashicorp/terraform-registry-address v0.2.5 h1:2GTftHqmUhVOeuu9CW3kwDkRe4pcBDq0uuK5VJngU1M=
128-
github.com/hashicorp/terraform-registry-address v0.2.5/go.mod h1:PpzXWINwB5kuVS5CA7m1+eO2f1jKb5ZDIxrOPfpnGkg=
129-
github.com/hashicorp/terraform-schema v0.0.0-20250117153811-3c4991466f2c h1:g/Y0BUI5Gk1hgMWcI5PpeXtvvmzvQruW6az0yPhFFKk=
130-
github.com/hashicorp/terraform-schema v0.0.0-20250117153811-3c4991466f2c/go.mod h1:+fQEDxf+c6PnG7/3ZF26K69zWLnIp/uTmsMffCsuw6o=
127+
github.com/hashicorp/terraform-registry-address v0.3.0 h1:HMpK3nqaGFPS9VmgRXrJL/dzHNdheGVKk5k7VlFxzCo=
128+
github.com/hashicorp/terraform-registry-address v0.3.0/go.mod h1:jRGCMiLaY9zii3GLC7hqpSnwhfnCN5yzvY0hh4iCGbM=
129+
github.com/hashicorp/terraform-schema v0.0.0-20250616093749-5d8128deffd1 h1:6ncaZLzLWeXs9J++HtPoJpwg3ZJ4mDEFMqpj3ol9ehc=
130+
github.com/hashicorp/terraform-schema v0.0.0-20250616093749-5d8128deffd1/go.mod h1:si3wjikcavAEF1QIx+p+tk5EvVubBpzu9sl8YasITTs=
131+
github.com/hashicorp/terraform-schema v0.0.0-20250616115602-34f2164294a0 h1:fpu271clSg0mDkfy7CYr1fs3ntT9AEioutKZR5r1n2s=
132+
github.com/hashicorp/terraform-schema v0.0.0-20250616115602-34f2164294a0/go.mod h1:si3wjikcavAEF1QIx+p+tk5EvVubBpzu9sl8YasITTs=
131133
github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ=
132134
github.com/hashicorp/terraform-svchost v0.1.1/go.mod h1:mNsjQfZyf/Jhz35v6/0LWcv26+X7JPS+buii2c9/ctc=
133135
github.com/hexops/autogold v1.3.1 h1:YgxF9OHWbEIUjhDbpnLhgVsjUDsiHDTyDfy2lrfdlzo=

internal/features/stacks/ast/stacks.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,11 @@ func (mf StackFilename) IsIgnored() bool {
3232
}
3333

3434
func IsStackFilename(name string) bool {
35-
return strings.HasSuffix(name, ".tfstack.hcl") ||
35+
return strings.HasSuffix(name, ".tfcomponent.hcl") ||
36+
strings.HasSuffix(name, ".tfcomponent.json") ||
37+
strings.HasSuffix(name, ".tfstack.hcl") ||
3638
strings.HasSuffix(name, ".tfstack.json")
39+
3740
}
3841

3942
// DeployFilename is a custom type for deployment files

internal/features/stacks/jobs/parse_stack.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import (
2020
)
2121

2222
// ParseStackConfiguration parses the whole Stack configuration,
23-
// i.e. turns bytes of `*.tfstack.hcl` & `*.tfdeploy.hcl` files into AST ([*hcl.File]).
23+
// i.e. turns bytes of `*.tfcomponent.hcl`, `*.tfstack.hcl` & `*.tfdeploy.hcl` files into AST ([*hcl.File]).
2424
func ParseStackConfiguration(ctx context.Context, fs ReadOnlyFS, stackStore *state.StackStore, stackPath string) error {
2525
record, err := stackStore.StackRecordByPath(stackPath)
2626
if err != nil {

internal/features/stacks/jobs/parse_test.go

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,21 @@ import (
2020
)
2121

2222
func TestParseStackConfiguration(t *testing.T) {
23+
runTestParseStackConfiguration(t, struct {
24+
folderName string
25+
extension string
26+
}{folderName: "simple-stack", extension: "tfcomponent.hcl"})
27+
28+
runTestParseStackConfiguration(t, struct {
29+
folderName string
30+
extension string
31+
}{folderName: "simple-stack-legacy-extension", extension: "tfstack.hcl"})
32+
}
33+
34+
func runTestParseStackConfiguration(t *testing.T, tc struct {
35+
folderName string
36+
extension string
37+
}) {
2338
ctx := context.Background()
2439
gs, err := globalState.NewStateStore()
2540
if err != nil {
@@ -36,7 +51,7 @@ func TestParseStackConfiguration(t *testing.T) {
3651
}
3752
testFs := filesystem.NewFilesystem(gs.DocumentStore)
3853

39-
simpleStackPath := filepath.Join(testData, "simple-stack")
54+
simpleStackPath := filepath.Join(testData, tc.folderName)
4055

4156
err = ss.Add(simpleStackPath)
4257
if err != nil {
@@ -58,7 +73,7 @@ func TestParseStackConfiguration(t *testing.T) {
5873
ctx = job.WithIgnoreState(ctx, true)
5974

6075
// say we're coming from did_change request
61-
componentsURI, err := filepath.Abs(filepath.Join(simpleStackPath, "components.tfstack.hcl"))
76+
componentsURI, err := filepath.Abs(filepath.Join(simpleStackPath, "components."+tc.extension))
6277
if err != nil {
6378
t.Fatal(err)
6479
}
@@ -78,24 +93,24 @@ func TestParseStackConfiguration(t *testing.T) {
7893
t.Fatal(err)
7994
}
8095

81-
componentsFile := ast.StackFilename("components.tfstack.hcl")
82-
// test if components.tfstack.hcl is not the same as first seen
96+
componentsFile := ast.StackFilename("components." + tc.extension)
97+
// test if components.tfstack.hcl / components.tfcomponent.hcl is not the same as first seen
8398
if before.ParsedFiles[componentsFile] == after.ParsedFiles[componentsFile] {
8499
t.Fatal("file should mismatch")
85100
}
86101

87-
variablesFile := ast.StackFilename("variables.tfstack.hcl")
88-
// test if variables.tfstack.hcl is the same as first seen
102+
variablesFile := ast.StackFilename("variables." + tc.extension)
103+
// test if variables.tfstack.hcl / variables.tfcomponent.hcl is the same as first seen
89104
if before.ParsedFiles[variablesFile] != after.ParsedFiles[variablesFile] {
90105
t.Fatal("file mismatch")
91106
}
92107

93-
// examine diags should change for components.tfstack.hcl
108+
// examine diags should change for components.tfstack.hcl / components.tfcomponent.hcl
94109
if before.Diagnostics[globalAst.HCLParsingSource][componentsFile][0] == after.Diagnostics[globalAst.HCLParsingSource][componentsFile][0] {
95110
t.Fatal("diags should mismatch")
96111
}
97112

98-
// examine diags should not change for variables.tfstack.hcl
113+
// examine diags should not change for variables.tfstack.hcl / variables.tfcomponent.hcl
99114
if before.Diagnostics[globalAst.HCLParsingSource][variablesFile][0] != after.Diagnostics[globalAst.HCLParsingSource][variablesFile][0] {
100115
t.Fatal("diags should match")
101116
}

internal/features/stacks/jobs/schema_test.go

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ import (
2525
)
2626

2727
func TestPreloadEmbeddedSchema_basic(t *testing.T) {
28+
runTestPreloadEmbeddedSchema_basic(t, struct{ extension string }{extension: "tfstack.hcl"})
29+
runTestPreloadEmbeddedSchema_basic(t, struct{ extension string }{extension: "tfcomponent.hcl"})
30+
}
31+
32+
func runTestPreloadEmbeddedSchema_basic(t *testing.T, tc struct {
33+
extension string
34+
}) {
2835
ctx := context.Background()
2936
dataDir := "data"
3037
schemasFS := fstest.MapFS{
@@ -52,10 +59,10 @@ func TestPreloadEmbeddedSchema_basic(t *testing.T) {
5259
// These are somewhat awkward double entries
5360
// to account for io/fs and our own path separator differences
5461
// See https://github.com/hashicorp/terraform-ls/issues/1025
55-
stackPath + "/providers.tfstack.hcl": &fstest.MapFile{
62+
stackPath + "/providers." + tc.extension: &fstest.MapFile{
5663
Data: []byte{},
5764
},
58-
filepath.Join(stackPath, "providers.tfstack.hcl"): &fstest.MapFile{
65+
filepath.Join(stackPath, "providers."+tc.extension): &fstest.MapFile{
5966
Data: []byte(`required_providers {
6067
random = {
6168
source = "hashicorp/random"
@@ -105,6 +112,13 @@ func TestPreloadEmbeddedSchema_basic(t *testing.T) {
105112
}
106113

107114
func TestPreloadEmbeddedSchema_unknownProviderOnly(t *testing.T) {
115+
runTestPreloadEmbeddedSchema_unknownProviderOnly(t, struct{ extension string }{extension: "tfstack.hcl"})
116+
runTestPreloadEmbeddedSchema_unknownProviderOnly(t, struct{ extension string }{extension: "tfextension.hcl"})
117+
}
118+
119+
func runTestPreloadEmbeddedSchema_unknownProviderOnly(t *testing.T, tc struct {
120+
extension string
121+
}) {
108122
ctx := context.Background()
109123
dataDir := "data"
110124
schemasFS := fstest.MapFS{
@@ -125,10 +139,10 @@ func TestPreloadEmbeddedSchema_unknownProviderOnly(t *testing.T) {
125139
// These are somewhat awkward double entries
126140
// to account for io/fs and our own path separator differences
127141
// See https://github.com/hashicorp/terraform-ls/issues/1025
128-
stackPath + "/providers.tfstack.hcl": &fstest.MapFile{
142+
stackPath + "/providers." + tc.extension: &fstest.MapFile{
129143
Data: []byte{},
130144
},
131-
filepath.Join(stackPath, "providers.tfstack.hcl"): &fstest.MapFile{
145+
filepath.Join(stackPath, "providers."+tc.extension): &fstest.MapFile{
132146
Data: []byte(`required_providers {
133147
unknown = {
134148
source = "hashicorp/unknown"
@@ -160,6 +174,13 @@ func TestPreloadEmbeddedSchema_unknownProviderOnly(t *testing.T) {
160174
}
161175

162176
func TestPreloadEmbeddedSchema_idempotency(t *testing.T) {
177+
runTestPreloadEmbeddedSchema_idempotency(t, struct{ extension string }{extension: "tfstack.hcl"})
178+
runTestPreloadEmbeddedSchema_idempotency(t, struct{ extension string }{extension: "tfcomponent.hcl"})
179+
}
180+
181+
func runTestPreloadEmbeddedSchema_idempotency(t *testing.T, tc struct {
182+
extension string
183+
}) {
163184
ctx := context.Background()
164185
dataDir := "data"
165186
schemasFS := fstest.MapFS{
@@ -187,10 +208,10 @@ func TestPreloadEmbeddedSchema_idempotency(t *testing.T) {
187208
// These are somewhat awkward two entries
188209
// to account for io/fs and our own path separator differences
189210
// See https://github.com/hashicorp/terraform-ls/issues/1025
190-
stackPath + "/providers.tfstack.hcl": &fstest.MapFile{
211+
stackPath + "/providers." + tc.extension: &fstest.MapFile{
191212
Data: []byte{},
192213
},
193-
filepath.Join(stackPath, "providers.tfstack.hcl"): &fstest.MapFile{
214+
filepath.Join(stackPath, "providers."+tc.extension): &fstest.MapFile{
194215
Data: []byte(`required_providers {
195216
random = {
196217
source = "hashicorp/random"
@@ -242,6 +263,13 @@ func TestPreloadEmbeddedSchema_idempotency(t *testing.T) {
242263
}
243264

244265
func TestPreloadEmbeddedSchema_raceCondition(t *testing.T) {
266+
runTestPreloadEmbeddedSchema_raceCondition(t, struct{ extension string }{extension: "tfstack.hcl"})
267+
runTestPreloadEmbeddedSchema_raceCondition(t, struct{ extension string }{extension: "tfcomponent.hcl"})
268+
}
269+
270+
func runTestPreloadEmbeddedSchema_raceCondition(t *testing.T, tc struct {
271+
extension string
272+
}) {
245273
ctx := context.Background()
246274
dataDir := "data"
247275
schemasFS := fstest.MapFS{
@@ -269,10 +297,10 @@ func TestPreloadEmbeddedSchema_raceCondition(t *testing.T) {
269297
// These are somewhat awkward two entries
270298
// to account for io/fs and our own path separator differences
271299
// See https://github.com/hashicorp/terraform-ls/issues/1025
272-
stackPath + "/providers.tfstack.hcl": &fstest.MapFile{
300+
stackPath + "/providers." + tc.extension: &fstest.MapFile{
273301
Data: []byte{},
274302
},
275-
filepath.Join(stackPath, "providers.tfstack.hcl"): &fstest.MapFile{
303+
filepath.Join(stackPath, "providers."+tc.extension): &fstest.MapFile{
276304
Data: []byte(`required_providers {
277305
random = {
278306
source = "hashicorp/random"

0 commit comments

Comments
 (0)