Skip to content

Commit 39334ea

Browse files
committed
feat(tfsearch): TF-26847 TF-27074: Added: init search feature
1 parent ea87117 commit 39334ea

24 files changed

+2098
-17
lines changed
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: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
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/v2"
15+
"github.com/hashicorp/terraform-ls/internal/features/search/ast"
16+
"github.com/hashicorp/terraform-ls/internal/features/search/state"
17+
ilsp "github.com/hashicorp/terraform-ls/internal/lsp"
18+
tfaddr "github.com/hashicorp/terraform-registry-address"
19+
tfmod "github.com/hashicorp/terraform-schema/module"
20+
tfschema "github.com/hashicorp/terraform-schema/schema"
21+
searchSchema "github.com/hashicorp/terraform-schema/schema/search"
22+
tfsearch "github.com/hashicorp/terraform-schema/search"
23+
)
24+
25+
type PathReader struct {
26+
StateReader StateReader
27+
ModuleReader ModuleReader
28+
RootReader RootReader
29+
}
30+
31+
var _ decoder.PathReader = &PathReader{}
32+
33+
type CombinedReader struct {
34+
ModuleReader
35+
StateReader
36+
RootReader
37+
}
38+
39+
type StateReader interface {
40+
List() ([]*state.SearchRecord, error)
41+
SearchRecordByPath(modPath string) (*state.SearchRecord, error)
42+
ProviderSchema(modPath string, addr tfaddr.Provider, vc version.Constraints) (*tfschema.ProviderSchema, error)
43+
}
44+
45+
type ModuleReader interface {
46+
// LocalModuleMeta returns the module meta data for a local module. This is the result
47+
// of the [earlydecoder] when processing module files
48+
LocalModuleMeta(modPath string) (*tfmod.Meta, error)
49+
}
50+
51+
type RootReader interface {
52+
InstalledModulePath(rootPath string, normalizedSource string) (string, bool)
53+
}
54+
55+
// PathContext returns a PathContext for the given path based on the language ID
56+
func (pr *PathReader) PathContext(path lang.Path) (*decoder.PathContext, error) {
57+
record, err := pr.StateReader.SearchRecordByPath(path.Path)
58+
if err != nil {
59+
return nil, err
60+
}
61+
62+
switch path.LanguageID {
63+
case ilsp.Search.String():
64+
return searchPathContext(record, CombinedReader{
65+
StateReader: pr.StateReader,
66+
ModuleReader: pr.ModuleReader,
67+
RootReader: pr.RootReader,
68+
})
69+
}
70+
71+
return nil, fmt.Errorf("unknown language ID: %q", path.LanguageID)
72+
}
73+
74+
func searchPathContext(record *state.SearchRecord, stateReader CombinedReader) (*decoder.PathContext, error) {
75+
// TODO: this should only work for terraform 1.8 and above
76+
version := record.RequiredTerraformVersion
77+
if version == nil {
78+
version = tfschema.LatestAvailableVersion
79+
}
80+
81+
schema, err := searchSchema.CoreSearchSchemaForVersion(version)
82+
if err != nil {
83+
return nil, err
84+
}
85+
86+
sm := searchSchema.NewSearchSchemaMerger(schema)
87+
sm.SetStateReader(stateReader)
88+
89+
meta := &tfsearch.Meta{
90+
Path: record.Path(),
91+
Lists: record.Meta.Lists,
92+
Variables: record.Meta.Variables,
93+
Filenames: record.Meta.Filenames,
94+
}
95+
96+
mergedSchema, err := sm.SchemaForSearch(meta)
97+
if err != nil {
98+
return nil, err
99+
}
100+
101+
pathCtx := &decoder.PathContext{
102+
Schema: mergedSchema,
103+
ReferenceOrigins: make(reference.Origins, 0),
104+
ReferenceTargets: make(reference.Targets, 0),
105+
Files: make(map[string]*hcl.File, 0),
106+
Validators: searchValidators,
107+
}
108+
109+
// TODO: Add reference origins and targets if needed
110+
for _, origin := range record.RefOrigins {
111+
if ast.IsSearchFilename(origin.OriginRange().Filename) {
112+
pathCtx.ReferenceOrigins = append(pathCtx.ReferenceOrigins, origin)
113+
}
114+
}
115+
116+
for _, target := range record.RefTargets {
117+
if target.RangePtr != nil && ast.IsSearchFilename(target.RangePtr.Filename) {
118+
pathCtx.ReferenceTargets = append(pathCtx.ReferenceTargets, target)
119+
} else if target.RangePtr == nil {
120+
pathCtx.ReferenceTargets = append(pathCtx.ReferenceTargets, target)
121+
}
122+
}
123+
124+
for name, f := range record.ParsedFiles {
125+
if _, ok := name.(ast.SearchFilename); ok {
126+
pathCtx.Files[name.String()] = f
127+
}
128+
}
129+
130+
return pathCtx, nil
131+
}
132+
133+
func (pr *PathReader) Paths(ctx context.Context) []lang.Path {
134+
paths := make([]lang.Path, 0)
135+
136+
searchRecords, err := pr.StateReader.List()
137+
if err != nil {
138+
return paths
139+
}
140+
141+
for _, record := range searchRecords {
142+
foundSearch := false
143+
for name := range record.ParsedFiles {
144+
if _, ok := name.(ast.SearchFilename); ok {
145+
foundSearch = true
146+
}
147+
148+
}
149+
150+
if foundSearch {
151+
paths = append(paths, lang.Path{
152+
Path: record.Path(),
153+
LanguageID: ilsp.Search.String(),
154+
})
155+
}
156+
157+
}
158+
159+
return paths
160+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package decoder
5+
6+
import (
7+
"github.com/hashicorp/hcl-lang/validator"
8+
)
9+
10+
var searchValidators = []validator.Validator{
11+
validator.BlockLabelsLength{},
12+
validator.DeprecatedAttribute{},
13+
validator.DeprecatedBlock{},
14+
validator.MaxBlocks{},
15+
validator.MinBlocks{},
16+
validator.MissingRequiredAttribute{},
17+
validator.UnexpectedAttribute{},
18+
validator.UnexpectedBlock{},
19+
}

0 commit comments

Comments
 (0)