Skip to content

Support for Terraform Search (.tfquery.hcl) files #2007

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
6a8306a
feat(tfsearch): TF-26847 TF-27074: Added: init search feature
anubhav-goel Jul 2, 2025
cfb1a68
feat(tfsearch): TF-26847 TF-27074: Modified: renamed file name
anubhav-goel Jul 7, 2025
5aaf299
feat(tfsearch): TF-26847 TF-27288: Added: dynamic schema for provider…
anubhav-goel Jul 30, 2025
b23cc59
Update metadata to update provider references correctly.
sunnyhashi Aug 5, 2025
4b97681
feat(tfsearch): TF-26847: Modified: terraform-json version to pseudo…
anubhav-goel Aug 6, 2025
892a892
feat(tfsearch): TF-26847: Modified: terraform-schema version to pseu…
anubhav-goel Aug 6, 2025
e87b916
feat(tfsearch): TF-26847: Added: changelog
anubhav-goel Aug 6, 2025
7a5f481
feat(tfsearch): TF-26847: Modified: terrform-schema version
anubhav-goel Aug 6, 2025
6091883
feat(tfsearch): TF-26847: Modified: terrform-schema version
anubhav-goel Aug 6, 2025
2ed7e57
feat(tfsearch): TF-26847: Modified: added terraform version in meta
anubhav-goel Aug 6, 2025
25fb048
feat(tfsearch): TF-26847: Modified: removed unused code
anubhav-goel Aug 7, 2025
631b6f8
feat(tfsearch): TF-26847: Modified: added test cases
anubhav-goel Aug 8, 2025
dbbe7ad
feat(tfsearch): TF-26847: Refactored: removed unused code
anubhav-goel Aug 11, 2025
2f2fc3b
feature/tfsearch-TF-26847: fix: Remove Deploy checks from search config
sunnyhashi Aug 12, 2025
d55cea6
feat(tfsearch): TF-26847 TF-27270: Modified: docs md files
anubhav-goel Aug 12, 2025
faa79d4
feat(tfsearch): TF-26847: Bumped: terraform-json
anubhav-goel Aug 13, 2025
c1652c2
feat(tfsearch): TF-26847: Bumped: terraform-schema
anubhav-goel Aug 13, 2025
48088c2
Merge branch 'main' into feature/tfsearch-TF-26847
anubhav-goel Aug 14, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changes/unreleased/ENHANCEMENTS-20250806-104456.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
kind: ENHANCEMENTS
body: Add support for Terraform Search files. This provides block and attribute completion, hover, and diagnostics along with syntax validation for Terraform Search files.
time: 2025-08-06T10:44:56.893693+05:30
custom:
Issue: "2007"
Repository: terraform-ls
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ require (
github.com/hashicorp/hcl-lang v0.0.0-20250613065305-ef4e1a57cead
github.com/hashicorp/hcl/v2 v2.24.0
github.com/hashicorp/terraform-exec v0.23.0
github.com/hashicorp/terraform-json v0.25.0
github.com/hashicorp/terraform-json v0.25.1-0.20250804121134-ec95f5b77511
github.com/hashicorp/terraform-registry-address v0.3.0
github.com/hashicorp/terraform-schema v0.0.0-20250616115602-34f2164294a0
github.com/hashicorp/terraform-schema v0.0.0-20250811094623-b86d395f9616
github.com/mcuadros/go-defaults v1.2.0
github.com/mh-cbon/go-fmt-fail v0.0.0-20160815164508-67765b3fbcb5
github.com/mitchellh/cli v1.1.5
Expand Down
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -122,12 +122,12 @@ github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQx
github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM=
github.com/hashicorp/terraform-exec v0.23.0 h1:MUiBM1s0CNlRFsCLJuM5wXZrzA3MnPYEsiXmzATMW/I=
github.com/hashicorp/terraform-exec v0.23.0/go.mod h1:mA+qnx1R8eePycfwKkCRk3Wy65mwInvlpAeOwmA7vlY=
github.com/hashicorp/terraform-json v0.25.0 h1:rmNqc/CIfcWawGiwXmRuiXJKEiJu1ntGoxseG1hLhoQ=
github.com/hashicorp/terraform-json v0.25.0/go.mod h1:sMKS8fiRDX4rVlR6EJUMudg1WcanxCMoWwTLkgZP/vc=
github.com/hashicorp/terraform-json v0.25.1-0.20250804121134-ec95f5b77511 h1:2roeYw1L7XQ9ggNMM/5YgPrbBpuh44uQk2bJh+7w8g8=
github.com/hashicorp/terraform-json v0.25.1-0.20250804121134-ec95f5b77511/go.mod h1:eyWCeC3nrZamyrKLFnrvwpc3LQPIJsx8hWHQ/nu2/v4=
github.com/hashicorp/terraform-registry-address v0.3.0 h1:HMpK3nqaGFPS9VmgRXrJL/dzHNdheGVKk5k7VlFxzCo=
github.com/hashicorp/terraform-registry-address v0.3.0/go.mod h1:jRGCMiLaY9zii3GLC7hqpSnwhfnCN5yzvY0hh4iCGbM=
github.com/hashicorp/terraform-schema v0.0.0-20250616115602-34f2164294a0 h1:fpu271clSg0mDkfy7CYr1fs3ntT9AEioutKZR5r1n2s=
github.com/hashicorp/terraform-schema v0.0.0-20250616115602-34f2164294a0/go.mod h1:si3wjikcavAEF1QIx+p+tk5EvVubBpzu9sl8YasITTs=
github.com/hashicorp/terraform-schema v0.0.0-20250811094623-b86d395f9616 h1:9hb9XBtSpp9rlR6gAFgXFy8wJE7W/woPwhkIfab4VuI=
github.com/hashicorp/terraform-schema v0.0.0-20250811094623-b86d395f9616/go.mod h1:Lye3Lm/aJnhNDGkzYg4BVxIxK95NT4gKdwto+xbDP+c=
github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ=
github.com/hashicorp/terraform-svchost v0.1.1/go.mod h1:mNsjQfZyf/Jhz35v6/0LWcv26+X7JPS+buii2c9/ctc=
github.com/hexops/autogold v1.3.1 h1:YgxF9OHWbEIUjhDbpnLhgVsjUDsiHDTyDfy2lrfdlzo=
Expand Down
119 changes: 119 additions & 0 deletions internal/features/search/ast/search.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package ast

import (
"strings"

"github.com/hashicorp/hcl/v2"
globalAst "github.com/hashicorp/terraform-ls/internal/terraform/ast"
)

type Filename interface {
String() string
IsJSON() bool
IsIgnored() bool
}

// SearchFilename is a custom type for search configuration files
type SearchFilename string

func (mf SearchFilename) String() string {
return string(mf)
}

func (mf SearchFilename) IsJSON() bool {
return strings.HasSuffix(string(mf), ".json")
}

func (mf SearchFilename) IsIgnored() bool {
return globalAst.IsIgnoredFile(string(mf))
}

func IsSearchFilename(name string) bool {
return strings.HasSuffix(name, ".tfquery.hcl") ||
strings.HasSuffix(name, ".tfquery.json")
}

// FilenameFromName returns a valid SearchFilename
func FilenameFromName(name string) Filename {
if IsSearchFilename(name) {
return SearchFilename(name)
}

return nil
}

type Files map[Filename]*hcl.File

func (sf Files) Copy() Files {
m := make(Files, len(sf))
for name, file := range sf {
m[name] = file
}
return m
}

func (mf Files) AsMap() map[string]*hcl.File {
m := make(map[string]*hcl.File, len(mf))
for name, file := range mf {
m[name.String()] = file
}
return m
}

type Diagnostics map[Filename]hcl.Diagnostics

func (sd Diagnostics) Copy() Diagnostics {
m := make(Diagnostics, len(sd))
for name, diags := range sd {
m[name] = diags
}
return m
}

// AutoloadedOnly returns only diagnostics that are not from ignored files
func (sd Diagnostics) AutoloadedOnly() Diagnostics {
diags := make(Diagnostics)
for name, f := range sd {
if !name.IsIgnored() {
diags[name] = f
}
}
return diags
}

func (sd Diagnostics) AsMap() map[string]hcl.Diagnostics {
m := make(map[string]hcl.Diagnostics, len(sd))
for name, diags := range sd {
m[name.String()] = diags
}
return m
}

func (sd Diagnostics) Count() int {
count := 0
for _, diags := range sd {
count += len(diags)
}
return count
}

func DiagnosticsFromMap(m map[string]hcl.Diagnostics) Diagnostics {
mf := make(Diagnostics, len(m))
for name, file := range m {
mf[FilenameFromName(name)] = file
}
return mf
}

type SourceDiagnostics map[globalAst.DiagnosticSource]Diagnostics

func (svd SourceDiagnostics) Count() int {
count := 0
for _, diags := range svd {
count += diags.Count()
}
return count
}
166 changes: 166 additions & 0 deletions internal/features/search/decoder/path_reader.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package decoder

import (
"context"
"fmt"

"github.com/hashicorp/go-version"
"github.com/hashicorp/hcl-lang/decoder"
"github.com/hashicorp/hcl-lang/lang"
"github.com/hashicorp/hcl-lang/reference"
"github.com/hashicorp/hcl-lang/schema"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform-ls/internal/features/search/ast"
"github.com/hashicorp/terraform-ls/internal/features/search/state"
ilsp "github.com/hashicorp/terraform-ls/internal/lsp"
tfaddr "github.com/hashicorp/terraform-registry-address"
tfmod "github.com/hashicorp/terraform-schema/module"
tfschema "github.com/hashicorp/terraform-schema/schema"
searchSchema "github.com/hashicorp/terraform-schema/schema/search"
tfsearch "github.com/hashicorp/terraform-schema/search"
)

type PathReader struct {
StateReader StateReader
ModuleReader ModuleReader
RootReader RootReader
}

var _ decoder.PathReader = &PathReader{}

type CombinedReader struct {
ModuleReader
StateReader
RootReader
}

type StateReader interface {
List() ([]*state.SearchRecord, error)
SearchRecordByPath(modPath string) (*state.SearchRecord, error)
ProviderSchema(modPath string, addr tfaddr.Provider, vc version.Constraints) (*tfschema.ProviderSchema, error)
}

type ModuleReader interface {
// LocalModuleMeta returns the module meta data for a local module. This is the result
// of the [earlydecoder] when processing module files
LocalModuleMeta(modPath string) (*tfmod.Meta, error)
}

type RootReader interface {
InstalledModulePath(rootPath string, normalizedSource string) (string, bool)

TerraformVersion(modPath string) *version.Version
}

// PathContext returns a PathContext for the given path based on the language ID
func (pr *PathReader) PathContext(path lang.Path) (*decoder.PathContext, error) {
record, err := pr.StateReader.SearchRecordByPath(path.Path)
if err != nil {
return nil, err
}

switch path.LanguageID {
case ilsp.Search.String():
return searchPathContext(record, CombinedReader{
StateReader: pr.StateReader,
ModuleReader: pr.ModuleReader,
RootReader: pr.RootReader,
})
}

return nil, fmt.Errorf("unknown language ID: %q", path.LanguageID)
}

func searchPathContext(record *state.SearchRecord, stateReader CombinedReader) (*decoder.PathContext, error) {
resolvedVersion := tfschema.ResolveVersion(stateReader.TerraformVersion(record.Path()), record.Meta.CoreRequirements)

sm := searchSchema.NewSearchSchemaMerger(mustCoreSchemaForVersion(resolvedVersion))
sm.SetStateReader(stateReader)

meta := &tfsearch.Meta{
Path: record.Path(),
CoreRequirements: record.Meta.CoreRequirements,
Lists: record.Meta.Lists,
Variables: record.Meta.Variables,
Filenames: record.Meta.Filenames,
ProviderReferences: record.Meta.ProviderReferences,
ProviderRequirements: record.Meta.ProviderRequirements,
}

mergedSchema, err := sm.SchemaForSearch(meta)
if err != nil {
return nil, err
}

pathCtx := &decoder.PathContext{
Schema: mergedSchema,
ReferenceOrigins: make(reference.Origins, 0),
ReferenceTargets: make(reference.Targets, 0),
Files: make(map[string]*hcl.File, 0),
Validators: searchValidators,
}

// TODO: Add reference origins and targets if needed
for _, origin := range record.RefOrigins {
if ast.IsSearchFilename(origin.OriginRange().Filename) {
pathCtx.ReferenceOrigins = append(pathCtx.ReferenceOrigins, origin)
}
}

for _, target := range record.RefTargets {
if target.RangePtr != nil && ast.IsSearchFilename(target.RangePtr.Filename) {
pathCtx.ReferenceTargets = append(pathCtx.ReferenceTargets, target)
} else if target.RangePtr == nil {
pathCtx.ReferenceTargets = append(pathCtx.ReferenceTargets, target)
}
}

for name, f := range record.ParsedFiles {
if _, ok := name.(ast.SearchFilename); ok {
pathCtx.Files[name.String()] = f
}
}

return pathCtx, nil
}

func (pr *PathReader) Paths(ctx context.Context) []lang.Path {
paths := make([]lang.Path, 0)

searchRecords, err := pr.StateReader.List()
if err != nil {
return paths
}

for _, record := range searchRecords {
foundSearch := false
for name := range record.ParsedFiles {
if _, ok := name.(ast.SearchFilename); ok {
foundSearch = true
}

}

if foundSearch {
paths = append(paths, lang.Path{
Path: record.Path(),
LanguageID: ilsp.Search.String(),
})
}

}

return paths
}

func mustCoreSchemaForVersion(v *version.Version) *schema.BodySchema {
s, err := searchSchema.CoreSearchSchemaForVersion(v)
if err != nil {
// this should never happen
panic(err)
}
return s
}
19 changes: 19 additions & 0 deletions internal/features/search/decoder/validators.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package decoder

import (
"github.com/hashicorp/hcl-lang/validator"
)

var searchValidators = []validator.Validator{
validator.BlockLabelsLength{},
validator.DeprecatedAttribute{},
validator.DeprecatedBlock{},
validator.MaxBlocks{},
validator.MinBlocks{},
validator.MissingRequiredAttribute{},
validator.UnexpectedAttribute{},
validator.UnexpectedBlock{},
}
Loading
Loading