Skip to content

Commit e5e41e0

Browse files
anubhav-goelsunnyhashijpogran-hashi
authored
Support for Terraform Search (.tfquery.hcl) files (#2007)
* feat(tfsearch): TF-26847 TF-27074: Added: init search feature * feat(tfsearch): TF-26847 TF-27074: Modified: renamed file name * feat(tfsearch): TF-26847 TF-27288: Added: dynamic schema for provider block * Update metadata to update provider references correctly. This reverts commit 21a998a. * feat(tfsearch): TF-26847: Modified: terraform-json version to pseudo-version * feat(tfsearch): TF-26847: Modified: terraform-schema version to pseudo-version * feat(tfsearch): TF-26847: Added: changelog * feat(tfsearch): TF-26847: Modified: terrform-schema version * feat(tfsearch): TF-26847: Modified: terrform-schema version * feat(tfsearch): TF-26847: Modified: added terraform version in meta * feat(tfsearch): TF-26847: Modified: removed unused code * feat(tfsearch): TF-26847: Modified: added test cases * feat(tfsearch): TF-26847: Refactored: removed unused code * feature/tfsearch-TF-26847: fix: Remove Deploy checks from search config * feat(tfsearch): TF-26847 TF-27270: Modified: docs md files * feat(tfsearch): TF-26847: Bumped: terraform-json * feat(tfsearch): TF-26847: Bumped: terraform-schema * feat(tfsearch): TF-26847: Fixed: typo in tfstack file regex Co-authored-by: James Pogran <[email protected]> * feat(tfsearch): TF-26847: Added: ReferenceValidation * feat(tfsearch): TF-26847: Added: MissingRequiredAttribute * feat(tfsearch): TF-26847: Refactored: renamed function name * feat(tfsearch): TF-26847: Bumped: terraform-schema version * feat(tfsearch): TF-26847: Fixed: test case * feat(tfsearch): TF-26847: Modified: removed corerequirements and providerrequirements from search meta * feat(tfsearch): TF-26847: Bumped: terraform-schema * feat(tfsearch): TF-26847: Bumped: terraform-schema --------- Co-authored-by: sunnyhashi <[email protected]> Co-authored-by: James Pogran <[email protected]>
1 parent 5639050 commit e5e41e0

40 files changed

+3994
-22
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: Add support for Terraform Search files. This provides block and attribute completion, hover, and diagnostics along with syntax validation for Terraform Search files.
3+
time: 2025-08-06T10:44:56.893693+05:30
4+
custom:
5+
Issue: "2007"
6+
Repository: terraform-ls

docs/USAGE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ The following filetypes are supported by the Terraform Language Server:
1010
- `terraform-vars` - variable files (`*.tfvars`)
1111
- `terraform-stack` - standard `*.tfcomponent.hcl` and `*.tfstack.hcl` files
1212
- `terraform-deploy` - standard `*.tfdeploy.hcl` files
13+
- `terraform-search` - standard `*.tfquery.hcl` files
1314

1415
_NOTE:_ Clients should be configured to follow the above language ID conventions
1516
and do **not** send `*.tf.json`, `*.tfvars.json` nor Packer HCL config

docs/architecture.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ We currently have several features:
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
6262
- `*.tfcomponent.hcl`, `*.tfstack.hcl` and `*.tfdeploy.hcl` files are handled in the `stacks` feature
63+
- `*.tfquery.hcl` files are handled in the `search` feature
6364

6465
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.
6566

@@ -93,13 +94,23 @@ The `jobs` package of each feature contains all the different indexing jobs need
9394
### Stack Feature Jobs
9495

9596
- `ParseStackConfiguration` - parses `*.tfcomponent.hcl`, `*.tfstack.hcl` and `*.tfdeploy.hcl` files to turn `[]byte` into `hcl` types (AST)
96-
- `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
97+
- `LoadStackMetadata` - uses [`earlydecoder`](https://pkg.go.dev/github.com/hashicorp/terraform-schema@main/earlydecoder/stacks) 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
9798
- `PreloadEmbeddedSchema` – loads provider schemas based on provider requirements from the bundled schemas
9899
- `DecodeReferenceTargets` - uses `hcl-lang` decoder to collect reference targets within `*.tfcomponent.hcl`, `*.tfstack.hcl` and `*.tfdeploy.hcl`
99100
- `DecodeReferenceOrigins` - uses `hcl-lang` decoder to collect reference origins within `*.tfcomponent.hcl`, `*.tfstack.hcl` and `*.tfdeploy.hcl`
100101
- `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
101102
- `ReferenceValidation` - does validation based on (mis)matched reference origins and targets, to flag up "orphaned" references
102103

104+
### Search Feature Jobs
105+
106+
- `ParseSearchConfiguration` - parses `*.tfquery.hcl` files to turn `[]byte` into `hcl` types (AST)
107+
- `LoadSearchMetadata` - uses [`earlydecoder`](https://pkg.go.dev/github.com/hashicorp/terraform-schema@main/earlydecoder/search) 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
108+
- `PreloadEmbeddedSchema` – loads provider schemas based on provider requirements from the bundled schemas
109+
- `DecodeReferenceTargets` - uses `hcl-lang` decoder to collect reference targets within `*.tfquery.hcl`
110+
- `DecodeReferenceOrigins` - uses `hcl-lang` decoder to collect reference origins within `*.tfquery.hcl`
111+
- `SchemaSearchValidation` - does schema-based validation of module files (`*.hcl.hcl`) and produces diagnostics associated with any "invalid" parts of code
112+
- `ReferenceValidation` - does validation based on (mis)matched reference origins and targets, to flag up "orphaned" references
113+
103114
### Adding a new feature / "language"
104115

105116
The existing `variables` feature is a good starting point when introducing a new language. Usually you need to roughly follow these steps to get a minimal working example:

docs/language-clients.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ The following file types are currently supported and language IDs expected:
1010
- `terraform-vars` - variable files (`*.tfvars`)
1111
- `terraform-stack` - standard `*.tfcomponent.hcl` and `*.tfstack.hcl` files
1212
- `terraform-deploy` - standard `*.tfdeploy.hcl` files
13+
- `terraform-search` - standard `*.tfquery.hcl` files
1314

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

4849
If the client implements file watcher, it should watch for any changes
49-
in `**/*.tf`, `**/*.tfvars`, `*.tfstack.hcl`, `**/*.tfstack.hcl` and `**/*.tfstack.hcl` files in the workspace.
50+
in `**/*.tf`, `**/*.tfvars`, `**/*.tfstack.hcl`, `**/*.tfcomponent.hcl`, `**/*.tfdeploy.hcl` and `**/*.tfquery.hcl` files in the workspace.
5051

5152
Client should **not** send changes for any other files.
5253

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ require (
1919
github.com/hashicorp/terraform-exec v0.23.0
2020
github.com/hashicorp/terraform-json v0.26.0
2121
github.com/hashicorp/terraform-registry-address v0.3.0
22-
github.com/hashicorp/terraform-schema v0.0.0-20250616115602-34f2164294a0
22+
github.com/hashicorp/terraform-schema v0.0.0-20250828062434-26de354af365
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: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,8 @@ github.com/hashicorp/terraform-json v0.26.0 h1:+BnJavhRH+oyNWPnfzrfQwVWCZBFMvjdi
126126
github.com/hashicorp/terraform-json v0.26.0/go.mod h1:eyWCeC3nrZamyrKLFnrvwpc3LQPIJsx8hWHQ/nu2/v4=
127127
github.com/hashicorp/terraform-registry-address v0.3.0 h1:HMpK3nqaGFPS9VmgRXrJL/dzHNdheGVKk5k7VlFxzCo=
128128
github.com/hashicorp/terraform-registry-address v0.3.0/go.mod h1:jRGCMiLaY9zii3GLC7hqpSnwhfnCN5yzvY0hh4iCGbM=
129-
github.com/hashicorp/terraform-schema v0.0.0-20250616115602-34f2164294a0 h1:fpu271clSg0mDkfy7CYr1fs3ntT9AEioutKZR5r1n2s=
130-
github.com/hashicorp/terraform-schema v0.0.0-20250616115602-34f2164294a0/go.mod h1:si3wjikcavAEF1QIx+p+tk5EvVubBpzu9sl8YasITTs=
129+
github.com/hashicorp/terraform-schema v0.0.0-20250828062434-26de354af365 h1:BvAqqJmgvh5ga1Ivrmta3V35oMusrOtvCdl+J/kWoSw=
130+
github.com/hashicorp/terraform-schema v0.0.0-20250828062434-26de354af365/go.mod h1:nnx41+GPagX9rK6V0ZLKAM+ws5nPxO1G50DhXt44ZhQ=
131131
github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ=
132132
github.com/hashicorp/terraform-svchost v0.1.1/go.mod h1:mNsjQfZyf/Jhz35v6/0LWcv26+X7JPS+buii2c9/ctc=
133133
github.com/hexops/autogold v1.3.1 h1:YgxF9OHWbEIUjhDbpnLhgVsjUDsiHDTyDfy2lrfdlzo=
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package ast
5+
6+
import (
7+
"strings"
8+
9+
"github.com/hashicorp/hcl/v2"
10+
globalAst "github.com/hashicorp/terraform-ls/internal/terraform/ast"
11+
)
12+
13+
type Filename interface {
14+
String() string
15+
IsJSON() bool
16+
IsIgnored() bool
17+
}
18+
19+
// SearchFilename is a custom type for search configuration files
20+
type SearchFilename string
21+
22+
func (mf SearchFilename) String() string {
23+
return string(mf)
24+
}
25+
26+
func (mf SearchFilename) IsJSON() bool {
27+
return strings.HasSuffix(string(mf), ".json")
28+
}
29+
30+
func (mf SearchFilename) IsIgnored() bool {
31+
return globalAst.IsIgnoredFile(string(mf))
32+
}
33+
34+
func IsSearchFilename(name string) bool {
35+
return strings.HasSuffix(name, ".tfquery.hcl") ||
36+
strings.HasSuffix(name, ".tfquery.json")
37+
}
38+
39+
// FilenameFromName returns a valid SearchFilename
40+
func FilenameFromName(name string) Filename {
41+
if IsSearchFilename(name) {
42+
return SearchFilename(name)
43+
}
44+
45+
return nil
46+
}
47+
48+
type Files map[Filename]*hcl.File
49+
50+
func (sf Files) Copy() Files {
51+
m := make(Files, len(sf))
52+
for name, file := range sf {
53+
m[name] = file
54+
}
55+
return m
56+
}
57+
58+
func (mf Files) AsMap() map[string]*hcl.File {
59+
m := make(map[string]*hcl.File, len(mf))
60+
for name, file := range mf {
61+
m[name.String()] = file
62+
}
63+
return m
64+
}
65+
66+
type Diagnostics map[Filename]hcl.Diagnostics
67+
68+
func (sd Diagnostics) Copy() Diagnostics {
69+
m := make(Diagnostics, len(sd))
70+
for name, diags := range sd {
71+
m[name] = diags
72+
}
73+
return m
74+
}
75+
76+
// AutoloadedOnly returns only diagnostics that are not from ignored files
77+
func (sd Diagnostics) AutoloadedOnly() Diagnostics {
78+
diags := make(Diagnostics)
79+
for name, f := range sd {
80+
if !name.IsIgnored() {
81+
diags[name] = f
82+
}
83+
}
84+
return diags
85+
}
86+
87+
func (sd Diagnostics) AsMap() map[string]hcl.Diagnostics {
88+
m := make(map[string]hcl.Diagnostics, len(sd))
89+
for name, diags := range sd {
90+
m[name.String()] = diags
91+
}
92+
return m
93+
}
94+
95+
func (sd Diagnostics) Count() int {
96+
count := 0
97+
for _, diags := range sd {
98+
count += len(diags)
99+
}
100+
return count
101+
}
102+
103+
func DiagnosticsFromMap(m map[string]hcl.Diagnostics) Diagnostics {
104+
mf := make(Diagnostics, len(m))
105+
for name, file := range m {
106+
mf[FilenameFromName(name)] = file
107+
}
108+
return mf
109+
}
110+
111+
type SourceDiagnostics map[globalAst.DiagnosticSource]Diagnostics
112+
113+
func (svd SourceDiagnostics) Count() int {
114+
count := 0
115+
for _, diags := range svd {
116+
count += diags.Count()
117+
}
118+
return count
119+
}
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package decoder
5+
6+
import (
7+
"context"
8+
"fmt"
9+
10+
"github.com/hashicorp/go-version"
11+
"github.com/hashicorp/hcl-lang/decoder"
12+
"github.com/hashicorp/hcl-lang/lang"
13+
"github.com/hashicorp/hcl-lang/reference"
14+
"github.com/hashicorp/hcl-lang/schema"
15+
"github.com/hashicorp/hcl/v2"
16+
"github.com/hashicorp/terraform-ls/internal/features/search/ast"
17+
"github.com/hashicorp/terraform-ls/internal/features/search/state"
18+
ilsp "github.com/hashicorp/terraform-ls/internal/lsp"
19+
tfaddr "github.com/hashicorp/terraform-registry-address"
20+
tfmod "github.com/hashicorp/terraform-schema/module"
21+
tfschema "github.com/hashicorp/terraform-schema/schema"
22+
searchSchema "github.com/hashicorp/terraform-schema/schema/search"
23+
tfsearch "github.com/hashicorp/terraform-schema/search"
24+
)
25+
26+
type PathReader struct {
27+
StateReader StateReader
28+
ModuleReader ModuleReader
29+
RootReader RootReader
30+
}
31+
32+
var _ decoder.PathReader = &PathReader{}
33+
34+
type CombinedReader struct {
35+
ModuleReader
36+
StateReader
37+
RootReader
38+
}
39+
40+
type StateReader interface {
41+
List() ([]*state.SearchRecord, error)
42+
GetSearchRecordByPath(modPath string) (*state.SearchRecord, error)
43+
ProviderSchema(modPath string, addr tfaddr.Provider, vc version.Constraints) (*tfschema.ProviderSchema, error)
44+
}
45+
46+
type ModuleReader interface {
47+
// LocalModuleMeta returns the module meta data for a local module. This is the result
48+
// of the [earlydecoder] when processing module files
49+
LocalModuleMeta(modPath string) (*tfmod.Meta, error)
50+
}
51+
52+
type RootReader interface {
53+
InstalledModulePath(rootPath string, normalizedSource string) (string, bool)
54+
55+
TerraformVersion(modPath string) *version.Version
56+
}
57+
58+
// PathContext returns a PathContext for the given path based on the language ID
59+
func (pr *PathReader) PathContext(path lang.Path) (*decoder.PathContext, error) {
60+
record, err := pr.StateReader.GetSearchRecordByPath(path.Path)
61+
if err != nil {
62+
return nil, err
63+
}
64+
65+
switch path.LanguageID {
66+
case ilsp.Search.String():
67+
return searchPathContext(record, CombinedReader{
68+
StateReader: pr.StateReader,
69+
ModuleReader: pr.ModuleReader,
70+
RootReader: pr.RootReader,
71+
})
72+
}
73+
74+
return nil, fmt.Errorf("unknown language ID: %q", path.LanguageID)
75+
}
76+
77+
func searchPathContext(record *state.SearchRecord, stateReader CombinedReader) (*decoder.PathContext, error) {
78+
resolvedVersion := tfschema.ResolveVersion(stateReader.TerraformVersion(record.Path()), record.Meta.CoreRequirements)
79+
80+
sm := searchSchema.NewSearchSchemaMerger(mustCoreSchemaForVersion(resolvedVersion))
81+
sm.SetStateReader(stateReader)
82+
83+
meta := &tfsearch.Meta{
84+
Path: record.Path(),
85+
Lists: record.Meta.Lists,
86+
Variables: record.Meta.Variables,
87+
Filenames: record.Meta.Filenames,
88+
ProviderReferences: record.Meta.ProviderReferences,
89+
}
90+
91+
mergedSchema, err := sm.SchemaForSearch(meta)
92+
if err != nil {
93+
return nil, err
94+
}
95+
96+
pathCtx := &decoder.PathContext{
97+
Schema: mergedSchema,
98+
ReferenceOrigins: make(reference.Origins, 0),
99+
ReferenceTargets: make(reference.Targets, 0),
100+
Files: make(map[string]*hcl.File, 0),
101+
Validators: searchValidators,
102+
}
103+
104+
// TODO: Add reference origins and targets if needed
105+
for _, origin := range record.RefOrigins {
106+
if ast.IsSearchFilename(origin.OriginRange().Filename) {
107+
pathCtx.ReferenceOrigins = append(pathCtx.ReferenceOrigins, origin)
108+
}
109+
}
110+
111+
for _, target := range record.RefTargets {
112+
if target.RangePtr != nil && ast.IsSearchFilename(target.RangePtr.Filename) {
113+
pathCtx.ReferenceTargets = append(pathCtx.ReferenceTargets, target)
114+
} else if target.RangePtr == nil {
115+
pathCtx.ReferenceTargets = append(pathCtx.ReferenceTargets, target)
116+
}
117+
}
118+
119+
for name, f := range record.ParsedFiles {
120+
if _, ok := name.(ast.SearchFilename); ok {
121+
pathCtx.Files[name.String()] = f
122+
}
123+
}
124+
125+
return pathCtx, nil
126+
}
127+
128+
func (pr *PathReader) Paths(ctx context.Context) []lang.Path {
129+
paths := make([]lang.Path, 0)
130+
131+
searchRecords, err := pr.StateReader.List()
132+
if err != nil {
133+
return paths
134+
}
135+
136+
for _, record := range searchRecords {
137+
foundSearch := false
138+
for name := range record.ParsedFiles {
139+
if _, ok := name.(ast.SearchFilename); ok {
140+
foundSearch = true
141+
}
142+
143+
}
144+
145+
if foundSearch {
146+
paths = append(paths, lang.Path{
147+
Path: record.Path(),
148+
LanguageID: ilsp.Search.String(),
149+
})
150+
}
151+
152+
}
153+
154+
return paths
155+
}
156+
157+
func mustCoreSchemaForVersion(v *version.Version) *schema.BodySchema {
158+
s, err := searchSchema.CoreSearchSchemaForVersion(v)
159+
if err != nil {
160+
// this should never happen
161+
panic(err)
162+
}
163+
return s
164+
}

0 commit comments

Comments
 (0)