diff --git a/decoder/block_candidates.go b/decoder/block_candidates.go index 503d2a69..16b7b598 100644 --- a/decoder/block_candidates.go +++ b/decoder/block_candidates.go @@ -12,7 +12,7 @@ import ( // blockSchemaToCandidate generates a lang.Candidate used for auto-complete inside an editor from a BlockSchema. // If `prefillRequiredFields` is `false`, it returns a snippet that does not expect any prefilled fields. // If `prefillRequiredFields` is `true`, it returns a snippet that is compatiable with a list of prefilled fields from `generateRequiredFieldsSnippet` -func (d *Decoder) blockSchemaToCandidate(blockType string, block *schema.BlockSchema, rng hcl.Range) lang.Candidate { +func (d *PathDecoder) blockSchemaToCandidate(blockType string, block *schema.BlockSchema, rng hcl.Range) lang.Candidate { triggerSuggest := false if len(block.Labels) > 0 { // We make some naive assumptions here for simplicity diff --git a/decoder/body_candidates.go b/decoder/body_candidates.go index 5f95c7c4..6f388f96 100644 --- a/decoder/body_candidates.go +++ b/decoder/body_candidates.go @@ -11,7 +11,7 @@ import ( ) // bodySchemaCandidates returns candidates for completion of fields inside a body or block. -func (d *Decoder) bodySchemaCandidates(body *hclsyntax.Body, schema *schema.BodySchema, prefixRng, editRng hcl.Range) lang.Candidates { +func (d *PathDecoder) bodySchemaCandidates(body *hclsyntax.Body, schema *schema.BodySchema, prefixRng, editRng hcl.Range) lang.Candidates { prefix, _ := d.bytesFromRange(prefixRng) candidates := lang.NewCandidates() diff --git a/decoder/body_candidates_test.go b/decoder/body_candidates_test.go index 742c8ff2..68be8aca 100644 --- a/decoder/body_candidates_test.go +++ b/decoder/body_candidates_test.go @@ -30,19 +30,18 @@ func TestDecoder_CandidateAtPos_incompleteAttributes(t *testing.T) { }, } - d := NewDecoder() - d.maxCandidates = 1 - - d.SetSchema(bodySchema) - f, _ := hclsyntax.ParseConfig([]byte(`customblock "label1" { attr } `), "test.tf", hcl.InitialPos) - err := d.LoadFile("test.tf", f) - if err != nil { - t.Fatal(err) - } + + d := testPathDecoder(t, &PathContext{ + Schema: bodySchema, + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) + d.maxCandidates = 1 candidates, err := d.CandidatesAtPos("test.tf", hcl.Pos{ Line: 2, @@ -103,17 +102,16 @@ func TestDecoder_CandidateAtPos_computedAttributes(t *testing.T) { }, } - d := NewDecoder() - d.SetSchema(bodySchema) - f, _ := hclsyntax.ParseConfig([]byte(`customblock "label1" { attr } `), "test.tf", hcl.InitialPos) - err := d.LoadFile("test.tf", f) - if err != nil { - t.Fatal(err) - } + d := testPathDecoder(t, &PathContext{ + Schema: bodySchema, + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) candidates, err := d.CandidatesAtPos("test.tf", hcl.Pos{ Line: 2, @@ -175,20 +173,18 @@ func TestDecoder_CandidateAtPos_incompleteBlocks(t *testing.T) { }, } - d := NewDecoder() - d.maxCandidates = 1 - - d.SetSchema(bodySchema) - f, _ := hclsyntax.ParseConfig([]byte(`customblock "label1" { block1 {} block } `), "test.tf", hcl.InitialPos) - err := d.LoadFile("test.tf", f) - if err != nil { - t.Fatal(err) - } + d := testPathDecoder(t, &PathContext{ + Schema: bodySchema, + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) + d.maxCandidates = 1 candidates, err := d.CandidatesAtPos("test.tf", hcl.Pos{ Line: 3, @@ -252,14 +248,15 @@ func TestDecoder_CandidateAtPos_duplicateNames(t *testing.T) { }, }, } - d := NewDecoder() - d.SetSchema(bodySchema) f, _ := hclsyntax.ParseConfig([]byte("\n"), "test.tf", hcl.InitialPos) - err := d.LoadFile("test.tf", f) - if err != nil { - t.Fatal(err) - } + + d := testPathDecoder(t, &PathContext{ + Schema: bodySchema, + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) candidates, err := d.CandidatesAtPos("test.tf", hcl.InitialPos) if err != nil { diff --git a/decoder/candidates.go b/decoder/candidates.go index e0f51a61..344d656a 100644 --- a/decoder/candidates.go +++ b/decoder/candidates.go @@ -13,7 +13,7 @@ import ( // // Schema is required in order to return any candidates and method will return // error if there isn't one. -func (d *Decoder) CandidatesAtPos(filename string, pos hcl.Pos) (lang.Candidates, error) { +func (d *PathDecoder) CandidatesAtPos(filename string, pos hcl.Pos) (lang.Candidates, error) { f, err := d.fileByName(filename) if err != nil { return lang.ZeroCandidates(), err @@ -24,10 +24,7 @@ func (d *Decoder) CandidatesAtPos(filename string, pos hcl.Pos) (lang.Candidates return lang.ZeroCandidates(), err } - d.rootSchemaMu.RLock() - defer d.rootSchemaMu.RUnlock() - - if d.rootSchema == nil { + if d.pathCtx.Schema == nil { return lang.ZeroCandidates(), &NoSchemaError{} } @@ -40,10 +37,10 @@ func (d *Decoder) CandidatesAtPos(filename string, pos hcl.Pos) (lang.Candidates outerBodyRng = ob.Range() } - return d.candidatesAtPos(rootBody, outerBodyRng, d.rootSchema, pos) + return d.candidatesAtPos(rootBody, outerBodyRng, d.pathCtx.Schema, pos) } -func (d *Decoder) candidatesAtPos(body *hclsyntax.Body, outerBodyRng hcl.Range, bodySchema *schema.BodySchema, pos hcl.Pos) (lang.Candidates, error) { +func (d *PathDecoder) candidatesAtPos(body *hclsyntax.Body, outerBodyRng hcl.Range, bodySchema *schema.BodySchema, pos hcl.Pos) (lang.Candidates, error) { if bodySchema == nil { return lang.ZeroCandidates(), nil } @@ -148,7 +145,7 @@ func (d *Decoder) candidatesAtPos(body *hclsyntax.Body, outerBodyRng hcl.Range, return d.bodySchemaCandidates(body, bodySchema, rng, rng), nil } -func (d *Decoder) isPosInsideAttrExpr(attr *hclsyntax.Attribute, pos hcl.Pos) bool { +func (d *PathDecoder) isPosInsideAttrExpr(attr *hclsyntax.Attribute, pos hcl.Pos) bool { if attr.Expr.Range().ContainsPos(pos) { return true } @@ -180,7 +177,7 @@ func (d *Decoder) isPosInsideAttrExpr(attr *hclsyntax.Attribute, pos hcl.Pos) bo return false } -func (d *Decoder) nameTokenRangeAtPos(filename string, pos hcl.Pos) (hcl.Range, error) { +func (d *PathDecoder) nameTokenRangeAtPos(filename string, pos hcl.Pos) (hcl.Range, error) { rng := hcl.Range{ Filename: filename, Start: pos, @@ -227,7 +224,7 @@ func nameTokenRangeAtPos(tokens hclsyntax.Tokens, pos hcl.Pos) (hcl.Range, error return hcl.Range{}, fmt.Errorf("no token found at %s", stringPos(pos)) } -func (d *Decoder) labelTokenRangeAtPos(filename string, pos hcl.Pos) (hcl.Range, error) { +func (d *PathDecoder) labelTokenRangeAtPos(filename string, pos hcl.Pos) (hcl.Range, error) { rng := hcl.Range{ Filename: filename, Start: pos, diff --git a/decoder/candidates_test.go b/decoder/candidates_test.go index e58e86dd..79733a9e 100644 --- a/decoder/candidates_test.go +++ b/decoder/candidates_test.go @@ -17,17 +17,18 @@ import ( ) func TestDecoder_CandidatesAtPos_noSchema(t *testing.T) { - d := NewDecoder() f, pDiags := hclsyntax.ParseConfig(testConfig, "test.tf", hcl.InitialPos) if len(pDiags) > 0 { t.Fatal(pDiags) } - err := d.LoadFile("test.tf", f) - if err != nil { - t.Fatal(err) - } - _, err = d.CandidatesAtPos("test.tf", hcl.InitialPos) + d := testPathDecoder(t, &PathContext{ + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) + + _, err := d.CandidatesAtPos("test.tf", hcl.InitialPos) noSchemaErr := &NoSchemaError{} if !errors.As(err, &noSchemaErr) { t.Fatal("expected NoSchemaError for no schema") @@ -35,16 +36,17 @@ func TestDecoder_CandidatesAtPos_noSchema(t *testing.T) { } func TestDecoder_CandidatesAtPos_emptyBody(t *testing.T) { - d := NewDecoder() f := &hcl.File{ Body: hcl.EmptyBody(), } - err := d.LoadFile("test.tf", f) - if err != nil { - t.Fatal(err) - } - _, err = d.CandidatesAtPos("test.tf", hcl.InitialPos) + d := testPathDecoder(t, &PathContext{ + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) + + _, err := d.CandidatesAtPos("test.tf", hcl.InitialPos) unknownFormatErr := &UnknownFileFormatError{} if !errors.As(err, &unknownFormatErr) { t.Fatal("expected UnknownFileFormatError for empty body") @@ -52,7 +54,6 @@ func TestDecoder_CandidatesAtPos_emptyBody(t *testing.T) { } func TestDecoder_CandidatesAtPos_json(t *testing.T) { - d := NewDecoder() f, pDiags := json.Parse([]byte(`{ "customblock": { "label1": {} @@ -61,12 +62,14 @@ func TestDecoder_CandidatesAtPos_json(t *testing.T) { if len(pDiags) > 0 { t.Fatal(pDiags) } - err := d.LoadFile("test.tf.json", f) - if err != nil { - t.Fatal(err) - } - _, err = d.CandidatesAtPos("test.tf.json", hcl.InitialPos) + d := testPathDecoder(t, &PathContext{ + Files: map[string]*hcl.File{ + "test.tf.json": f, + }, + }) + + _, err := d.CandidatesAtPos("test.tf.json", hcl.InitialPos) unknownFormatErr := &UnknownFileFormatError{} if !errors.As(err, &unknownFormatErr) { t.Fatal("expected UnknownFileFormatError for JSON body") @@ -92,8 +95,6 @@ func TestDecoder_CandidatesAtPos_unknownBlock(t *testing.T) { }, } - d := NewDecoder() - d.SetSchema(bodySchema) f, pDiags := hclsyntax.ParseConfig([]byte(`customblock "label1" { } @@ -101,12 +102,15 @@ func TestDecoder_CandidatesAtPos_unknownBlock(t *testing.T) { if len(pDiags) > 0 { t.Fatal(pDiags) } - err := d.LoadFile("test.tf", f) - if err != nil { - t.Fatal(err) - } - _, err = d.CandidatesAtPos("test.tf", hcl.Pos{ + d := testPathDecoder(t, &PathContext{ + Schema: bodySchema, + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) + + _, err := d.CandidatesAtPos("test.tf", hcl.Pos{ Line: 2, Column: 1, Byte: 23, @@ -262,16 +266,17 @@ func TestDecoder_CandidatesAtPos_nilBodySchema(t *testing.T) { for i, tc := range testCases { t.Run(fmt.Sprintf("%d-%s", i, tc.name), func(t *testing.T) { - d := NewDecoder() - d.SetSchema(tc.rootSchema) f, pDiags := hclsyntax.ParseConfig([]byte(tc.config), "test.tf", hcl.InitialPos) if len(pDiags) > 0 { t.Fatal(pDiags) } - err := d.LoadFile("test.tf", f) - if err != nil { - t.Fatal(err) - } + + d := testPathDecoder(t, &PathContext{ + Schema: tc.rootSchema, + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) candidates, err := d.CandidatesAtPos("test.tf", tc.pos) if err != nil { @@ -304,14 +309,14 @@ func TestDecoder_CandidatesAtPos_prefixNearEOF(t *testing.T) { }, } - d := NewDecoder() - d.SetSchema(bodySchema) f, _ := hclsyntax.ParseConfig([]byte(`res`), "test.tf", hcl.InitialPos) - err := d.LoadFile("test.tf", f) - if err != nil { - t.Fatal(err) - } + d := testPathDecoder(t, &PathContext{ + Schema: bodySchema, + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) candidates, err := d.CandidatesAtPos("test.tf", hcl.Pos{ Line: 1, @@ -401,20 +406,21 @@ func TestDecoder_CandidatesAtPos_invalidBlockPositions(t *testing.T) { }, } - d := NewDecoder() - d.SetSchema(bodySchema) f, pDiags := hclsyntax.ParseConfig(testConfig, "test.tf", hcl.InitialPos) if len(pDiags) > 0 { t.Fatal(pDiags) } - err := d.LoadFile("test.tf", f) - if err != nil { - t.Fatal(err) - } + + d := testPathDecoder(t, &PathContext{ + Schema: bodySchema, + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) for i, tc := range testCases { t.Run(fmt.Sprintf("%d-%s", i, tc.name), func(t *testing.T) { - _, err = d.CandidatesAtPos("test.tf", tc.pos) + _, err := d.CandidatesAtPos("test.tf", tc.pos) if err == nil { t.Fatal("expected error") } @@ -448,13 +454,14 @@ func TestDecoder_CandidatesAtPos_rightHandSide(t *testing.T) { } `) - d := NewDecoder() - d.SetSchema(bodySchema) f, _ := hclsyntax.ParseConfig(testConfig, "test.tf", hcl.InitialPos) - err := d.LoadFile("test.tf", f) - if err != nil { - t.Fatal(err) - } + + d := testPathDecoder(t, &PathContext{ + Schema: bodySchema, + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) candidates, err := d.CandidatesAtPos("test.tf", hcl.Pos{ Line: 2, @@ -493,13 +500,14 @@ func TestDecoder_CandidatesAtPos_rightHandSideInString(t *testing.T) { } `) - d := NewDecoder() - d.SetSchema(bodySchema) f, _ := hclsyntax.ParseConfig(testConfig, "test.tf", hcl.InitialPos) - err := d.LoadFile("test.tf", f) - if err != nil { - t.Fatal(err) - } + + d := testPathDecoder(t, &PathContext{ + Schema: bodySchema, + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) candidates, err := d.CandidatesAtPos("test.tf", hcl.Pos{ Line: 2, @@ -553,13 +561,14 @@ func TestDecoder_CandidatesAtPos_endOfLabel(t *testing.T) { } `) - d := NewDecoder() - d.SetSchema(bodySchema) f, _ := hclsyntax.ParseConfig(testConfig, "test.tf", hcl.InitialPos) - err := d.LoadFile("test.tf", f) - if err != nil { - t.Fatal(err) - } + + d := testPathDecoder(t, &PathContext{ + Schema: bodySchema, + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) candidates, err := d.CandidatesAtPos("test.tf", hcl.Pos{ Line: 1, @@ -645,13 +654,14 @@ func TestDecoder_CandidatesAtPos_nonCompletableLabel(t *testing.T) { } `) - d := NewDecoder() - d.SetSchema(bodySchema) f, _ := hclsyntax.ParseConfig(testConfig, "test.tf", hcl.InitialPos) - err := d.LoadFile("test.tf", f) - if err != nil { - t.Fatal(err) - } + + d := testPathDecoder(t, &PathContext{ + Schema: bodySchema, + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) candidates, err := d.CandidatesAtPos("test.tf", hcl.Pos{ Line: 1, @@ -686,16 +696,17 @@ func TestDecoder_CandidatesAtPos_zeroByteContent(t *testing.T) { }, } - d := NewDecoder() - d.SetSchema(bodySchema) f, pDiags := hclsyntax.ParseConfig([]byte{}, "test.tf", hcl.InitialPos) if len(pDiags) > 0 { t.Fatal(pDiags) } - err := d.LoadFile("test.tf", f) - if err != nil { - t.Fatal(err) - } + + d := testPathDecoder(t, &PathContext{ + Schema: bodySchema, + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) candidates, err := d.CandidatesAtPos("test.tf", hcl.InitialPos) if err != nil { @@ -747,16 +758,17 @@ func TestDecoder_CandidatesAtPos_endOfFilePos(t *testing.T) { } `) - d := NewDecoder() - d.SetSchema(bodySchema) f, pDiags := hclsyntax.ParseConfig([]byte(cfg), "test.tf", hcl.InitialPos) if len(pDiags) > 0 { t.Fatal(pDiags) } - err := d.LoadFile("test.tf", f) - if err != nil { - t.Fatal(err) - } + + d := testPathDecoder(t, &PathContext{ + Schema: bodySchema, + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) candidates, err := d.CandidatesAtPos("test.tf", hcl.Pos{Line: 4, Column: 1, Byte: 52}) if err != nil { @@ -833,16 +845,17 @@ func TestDecoder_CandidatesAtPos_emptyLabel(t *testing.T) { } `) - d := NewDecoder() - d.SetSchema(bodySchema) f, pDiags := hclsyntax.ParseConfig([]byte(cfg), "test.tf", hcl.InitialPos) if len(pDiags) > 0 { t.Fatal(pDiags) } - err := d.LoadFile("test.tf", f) - if err != nil { - t.Fatal(err) - } + + d := testPathDecoder(t, &PathContext{ + Schema: bodySchema, + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) candidates, err := d.CandidatesAtPos("test.tf", hcl.Pos{Line: 1, Column: 11, Byte: 10}) if err != nil { @@ -938,16 +951,17 @@ func TestDecoder_CandidatesAtPos_emptyLabel_duplicateDepKeys(t *testing.T) { } `) - d := NewDecoder() - d.SetSchema(bodySchema) f, pDiags := hclsyntax.ParseConfig([]byte(cfg), "test.tf", hcl.InitialPos) if len(pDiags) > 0 { t.Fatal(pDiags) } - err := d.LoadFile("test.tf", f) - if err != nil { - t.Fatal(err) - } + + d := testPathDecoder(t, &PathContext{ + Schema: bodySchema, + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) candidates, err := d.CandidatesAtPos("test.tf", hcl.Pos{Line: 1, Column: 11, Byte: 10}) if err != nil { @@ -1040,17 +1054,17 @@ resource "random_resource" "test" { } `) - d := NewDecoder() - d.SetSchema(bodySchema) - f, pDiags := hclsyntax.ParseConfig(cfg, "test.tf", hcl.InitialPos) if len(pDiags) > 0 { t.Fatal(pDiags) } - err := d.LoadFile("test.tf", f) - if err != nil { - t.Fatal(err) - } + + d := testPathDecoder(t, &PathContext{ + Schema: bodySchema, + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) testCases := []struct { name string @@ -1317,17 +1331,17 @@ func TestDecoder_CandidatesAtPos_AnyAttribute(t *testing.T) { } `) - d := NewDecoder() - d.SetSchema(bodySchema) - f, pDiags := hclsyntax.ParseConfig(cfg, "test.tf", hcl.InitialPos) if len(pDiags) > 0 { t.Fatal(pDiags) } - err := d.LoadFile("test.tf", f) - if err != nil { - t.Fatal(err) - } + + d := testPathDecoder(t, &PathContext{ + Schema: bodySchema, + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) pos := hcl.Pos{Line: 2, Column: 1, Byte: 21} candidates, err := d.CandidatesAtPos("test.tf", pos) @@ -1389,17 +1403,17 @@ func TestDecoder_CandidatesAtPos_multipleTypes(t *testing.T) { } `) - d := NewDecoder() - d.SetSchema(bodySchema) - f, pDiags := hclsyntax.ParseConfig(cfg, "test.tf", hcl.InitialPos) if len(pDiags) > 0 { t.Fatal(pDiags) } - err := d.LoadFile("test.tf", f) - if err != nil { - t.Fatal(err) - } + + d := testPathDecoder(t, &PathContext{ + Schema: bodySchema, + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) pos := hcl.Pos{Line: 2, Column: 1, Byte: 38} candidates, err := d.CandidatesAtPos("test.tf", pos) @@ -1450,9 +1464,6 @@ func TestDecoder_CandidatesAtPos_incompleteAttrOrBlock(t *testing.T) { }, } - d := NewDecoder() - d.SetSchema(bodySchema) - testCases := []struct { name string src string @@ -1513,10 +1524,12 @@ resource "any" "ref" { t.Run(fmt.Sprintf("%d-%s", i, tc.name), func(t *testing.T) { f, _ := hclsyntax.ParseConfig([]byte(tc.src), "test.tf", hcl.InitialPos) - err := d.LoadFile("test.tf", f) - if err != nil { - t.Fatal(err) - } + d := testPathDecoder(t, &PathContext{ + Schema: bodySchema, + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) candidates, err := d.CandidatesAtPos("test.tf", tc.pos) if err != nil { @@ -1565,9 +1578,6 @@ func TestDecoder_CandidatesAtPos_incompleteLabel(t *testing.T) { }, } - d := NewDecoder() - d.SetSchema(bodySchema) - testCases := []struct { name string src string @@ -1628,10 +1638,12 @@ resource "any" "ref" { t.Run(fmt.Sprintf("%d-%s", i, tc.name), func(t *testing.T) { f, _ := hclsyntax.ParseConfig([]byte(tc.src), "test.tf", hcl.InitialPos) - err := d.LoadFile("test.tf", f) - if err != nil { - t.Fatal(err) - } + d := testPathDecoder(t, &PathContext{ + Schema: bodySchema, + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) candidates, err := d.CandidatesAtPos("test.tf", tc.pos) if err != nil { diff --git a/decoder/context.go b/decoder/context.go new file mode 100644 index 00000000..4fbf9934 --- /dev/null +++ b/decoder/context.go @@ -0,0 +1,15 @@ +package decoder + +type DecoderContext struct { + // UTM parameters for docs URLs + // utm_source parameter, typically language server identification + UtmSource string + // utm_medium parameter, typically language client identification + UtmMedium string + // utm_content parameter, e.g. documentHover or documentLink + UseUtmContent bool +} + +func (d *Decoder) SetContext(ctx DecoderContext) { + d.ctx = ctx +} diff --git a/decoder/decoder.go b/decoder/decoder.go index 9dedaa34..c2cef0b6 100644 --- a/decoder/decoder.go +++ b/decoder/decoder.go @@ -1,172 +1,34 @@ package decoder import ( + "context" "fmt" - "sort" - "sync" - "github.com/hashicorp/hcl-lang/lang" "github.com/hashicorp/hcl-lang/schema" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" ) type Decoder struct { - files map[string]*hcl.File - filesMu *sync.RWMutex - - refTargetReader ReferenceTargetReader - refOriginReader ReferenceOriginReader - rootSchema *schema.BodySchema - rootSchemaMu *sync.RWMutex - maxCandidates uint - - // UTM parameters for docs URLs - // utm_source parameter, typically language server identification - utmSource string - // utm_medium parameter, typically language client identification - utmMedium string - // utm_content parameter, e.g. documentHover or documentLink - useUtmContent bool - - // PrefillRequiredFields enriches label-based completion candidates with required attributes and blocks - PrefillRequiredFields bool + ctx DecoderContext + pathReader PathReader } -type ReferenceTargetReader func() lang.ReferenceTargets -type ReferenceOriginReader func() lang.ReferenceOrigins +type PathReader interface { + Paths() []string + PathContext(ctx context.Context, path string) *PathContext +} // NewDecoder creates a new Decoder // // Decoder is safe for use without any schema, but configuration files are loaded // via LoadFile and (optionally) schema is set via SetSchema. -func NewDecoder() *Decoder { +func NewDecoder(pathReader PathReader) *Decoder { return &Decoder{ - rootSchemaMu: &sync.RWMutex{}, - files: make(map[string]*hcl.File, 0), - filesMu: &sync.RWMutex{}, - maxCandidates: 100, + pathReader: pathReader, } } -// SetSchema sets the schema decoder uses for decoding the configuration -// -// This is useful for progressive enhancement experience, where a -// Decoder without schema can provide limited functionality (e.g. symbols), and -// the schema can be gradually enriched (e.g. Terraform core -> providers). -func (d *Decoder) SetSchema(schema *schema.BodySchema) { - d.rootSchemaMu.Lock() - defer d.rootSchemaMu.Unlock() - d.rootSchema = schema -} - -func (d *Decoder) SetReferenceTargetReader(f ReferenceTargetReader) { - d.refTargetReader = f -} - -func (d *Decoder) SetReferenceOriginReader(f ReferenceOriginReader) { - d.refOriginReader = f -} - -func (d *Decoder) SetUtmSource(src string) { - d.utmSource = src -} - -func (d *Decoder) SetUtmMedium(medium string) { - d.utmMedium = medium -} - -func (d *Decoder) UseUtmContent(use bool) { - d.useUtmContent = use -} - -// LoadFile loads a new (non-empty) parsed file -// -// e.g. result of hclsyntax.ParseConfig -func (d *Decoder) LoadFile(filename string, f *hcl.File) error { - d.filesMu.Lock() - defer d.filesMu.Unlock() - - if f == nil { - return fmt.Errorf("%s: invalid content provided", filename) - } - - if f.Body == nil { - return fmt.Errorf("%s: file has no body", filename) - } - - d.files[filename] = f - - return nil -} - -// Filenames returns a slice of filenames already loaded via LoadFile -func (p *Decoder) Filenames() []string { - p.filesMu.RLock() - defer p.filesMu.RUnlock() - - var files []string - for filename := range p.files { - files = append(files, filename) - } - - sort.Strings(files) - - return files -} - -func (d *Decoder) bytesForFile(file string) ([]byte, error) { - d.filesMu.RLock() - defer d.filesMu.RUnlock() - - f, ok := d.files[file] - if !ok { - return nil, &FileNotFoundError{Filename: file} - } - - return f.Bytes, nil -} - -func (d *Decoder) bytesFromRange(rng hcl.Range) ([]byte, error) { - b, err := d.bytesForFile(rng.Filename) - if err != nil { - return nil, err - } - - return rng.SliceBytes(b), nil -} - -func (d *Decoder) fileByName(name string) (*hcl.File, error) { - d.filesMu.RLock() - defer d.filesMu.RUnlock() - - f, ok := d.files[name] - if !ok { - return nil, &FileNotFoundError{Filename: name} - } - return f, nil -} - -func (d *Decoder) bodyForFileAndPos(name string, f *hcl.File, pos hcl.Pos) (*hclsyntax.Body, error) { - body, isHcl := f.Body.(*hclsyntax.Body) - if !isHcl { - return nil, &UnknownFileFormatError{Filename: name} - } - - if !body.Range().ContainsPos(pos) && - !posEqual(body.Range().Start, pos) && - !posEqual(body.Range().End, pos) { - - return nil, &PosOutOfRangeError{ - Filename: name, - Pos: pos, - Range: body.Range(), - } - } - - return body, nil -} - func posEqual(pos, other hcl.Pos) bool { return pos.Line == other.Line && pos.Column == other.Column && diff --git a/decoder/decoder_test.go b/decoder/decoder_test.go index 74eaac08..f79df3ce 100644 --- a/decoder/decoder_test.go +++ b/decoder/decoder_test.go @@ -1,6 +1,7 @@ package decoder import ( + "context" "fmt" "testing" @@ -12,29 +13,36 @@ import ( "github.com/zclconf/go-cty/cty" ) -func TestDecoder_LoadFile_nilFile(t *testing.T) { - d := NewDecoder() - err := d.LoadFile("test.tf", nil) - if err == nil { - t.Fatal("expected error for nil file") - } - if diff := cmp.Diff(`test.tf: invalid content provided`, err.Error()); diff != "" { - t.Fatalf("unexpected error: %s", diff) - } +type testPathReader struct { + paths map[string]*PathContext } -func TestDecoder_LoadFile_nilRootBody(t *testing.T) { - d := NewDecoder() - f := &hcl.File{ - Body: nil, - } - err := d.LoadFile("test.tf", f) - if err == nil { - t.Fatal("expected error for nil body") +func (r *testPathReader) Paths() []string { + dirs := make([]string, len(r.paths)) + + i := 0 + for dir := range r.paths { + dirs[i] = dir + i++ } - if diff := cmp.Diff(`test.tf: file has no body`, err.Error()); diff != "" { - t.Fatalf("unexpected error: %s", diff) + + return dirs +} + +func (r *testPathReader) PathContext(ctx context.Context, path string) *PathContext { + return r.paths[path] +} + +func testPathDecoder(t *testing.T, dirCtx *PathContext) *PathDecoder { + dirPath := t.TempDir() + dirCtx.DirPath = dirPath + dirs := map[string]*PathContext{ + dirPath: dirCtx, } + + return NewDecoder(&testPathReader{ + paths: dirs, + }).Path(context.Background(), dirPath) } func TestTraversalToAddress(t *testing.T) { diff --git a/decoder/expression_candidates.go b/decoder/expression_candidates.go index a6e6f129..9dc9d475 100644 --- a/decoder/expression_candidates.go +++ b/decoder/expression_candidates.go @@ -13,7 +13,7 @@ import ( "github.com/zclconf/go-cty/cty" ) -func (d *Decoder) attrValueCandidatesAtPos(attr *hclsyntax.Attribute, schema *schema.AttributeSchema, outerBodyRng hcl.Range, pos hcl.Pos) (lang.Candidates, error) { +func (d *PathDecoder) attrValueCandidatesAtPos(attr *hclsyntax.Attribute, schema *schema.AttributeSchema, outerBodyRng hcl.Range, pos hcl.Pos) (lang.Candidates, error) { constraints, editRng := constraintsAtPos(attr.Expr, ExprConstraints(schema.Expr), pos) if len(constraints) > 0 { prefixRng := editRng @@ -151,7 +151,7 @@ func constraintsAtPos(expr hcl.Expression, constraints ExprConstraints, pos hcl. return ExprConstraints{}, expr.Range() } -func (d *Decoder) expressionCandidatesAtPos(constraints ExprConstraints, outerBodyRng, prefixRng, editRng hcl.Range) (lang.Candidates, error) { +func (d *PathDecoder) expressionCandidatesAtPos(constraints ExprConstraints, outerBodyRng, prefixRng, editRng hcl.Range) (lang.Candidates, error) { candidates := lang.NewCandidates() for _, c := range constraints { @@ -162,7 +162,7 @@ func (d *Decoder) expressionCandidatesAtPos(constraints ExprConstraints, outerBo return candidates, nil } -func (d *Decoder) constraintToCandidates(constraint schema.ExprConstraint, outerBodyRng, prefixRng, editRng hcl.Range) []lang.Candidate { +func (d *PathDecoder) constraintToCandidates(constraint schema.ExprConstraint, outerBodyRng, prefixRng, editRng hcl.Range) []lang.Candidate { candidates := make([]lang.Candidate, 0) switch c := constraint.(type) { @@ -314,10 +314,10 @@ func (d *Decoder) constraintToCandidates(constraint schema.ExprConstraint, outer return candidates } -func (d *Decoder) candidatesForTraversalConstraint(tc schema.TraversalExpr, outerBodyRng, prefixRng, editRng hcl.Range) []lang.Candidate { +func (d *PathDecoder) candidatesForTraversalConstraint(tc schema.TraversalExpr, outerBodyRng, prefixRng, editRng hcl.Range) []lang.Candidate { candidates := make([]lang.Candidate, 0) - if d.refTargetReader == nil { + if d.pathCtx.ReferenceTargets == nil { return candidates } @@ -328,7 +328,7 @@ func (d *Decoder) candidatesForTraversalConstraint(tc schema.TraversalExpr, oute prefix, _ := d.bytesFromRange(prefixRng) - refs := ReferenceTargets(d.refTargetReader()) + refs := ReferenceTargets(d.pathCtx.ReferenceTargets) refs.MatchWalk(tc, string(prefix), func(ref lang.ReferenceTarget) error { // avoid suggesting references to block's own fields from within (for now) diff --git a/decoder/expression_candidates_test.go b/decoder/expression_candidates_test.go index 977be109..47f2a033 100644 --- a/decoder/expression_candidates_test.go +++ b/decoder/expression_candidates_test.go @@ -1,6 +1,7 @@ package decoder import ( + "context" "fmt" "testing" @@ -1504,16 +1505,17 @@ func TestDecoder_CandidateAtPos_expressions(t *testing.T) { for i, tc := range testCases { t.Run(fmt.Sprintf("%d-%s", i, tc.testName), func(t *testing.T) { - d := NewDecoder() - d.SetSchema(&schema.BodySchema{ + bodySchema := &schema.BodySchema{ Attributes: tc.attrSchema, - }) + } f, _ := hclsyntax.ParseConfig([]byte(tc.cfg), "test.tf", hcl.InitialPos) - err := d.LoadFile("test.tf", f) - if err != nil { - t.Fatal(err) - } + d := testPathDecoder(t, &PathContext{ + Schema: bodySchema, + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) candidates, err := d.CandidatesAtPos("test.tf", tc.pos) if err != nil { @@ -2527,20 +2529,25 @@ another_block "meh" { for i, tc := range testCases { t.Run(fmt.Sprintf("%d-%s", i, tc.testName), func(t *testing.T) { - d := NewDecoder() - d.SetSchema(tc.bodySchema) - d.SetReferenceTargetReader(func() lang.ReferenceTargets { - bRefs := tc.builtinRefs - refs, _ := d.CollectReferenceTargets() - refs = append(refs, bRefs...) - return refs - }) - f, _ := hclsyntax.ParseConfig([]byte(tc.cfg), "test.tf", hcl.InitialPos) - err := d.LoadFile("test.tf", f) - if err != nil { - t.Fatal(err) + + testDir := t.TempDir() + dirReader := &testPathReader{ + paths: map[string]*PathContext{ + testDir: &PathContext{ + DirPath: testDir, + Schema: tc.bodySchema, + Files: map[string]*hcl.File{ + "test.tf": f, + }, + ReferenceTargets: tc.builtinRefs, + }, + }, } + decoder := NewDecoder(dirReader) + refTargets, _ := decoder.Path(context.Background(), testDir).CollectReferenceTargets() + dirReader.paths[testDir].ReferenceTargets = append(dirReader.paths[testDir].ReferenceTargets, refTargets...) + d := decoder.Path(context.Background(), testDir) candidates, err := d.CandidatesAtPos("test.tf", tc.pos) if err != nil { diff --git a/decoder/hover.go b/decoder/hover.go index d5ba6394..29613d6b 100644 --- a/decoder/hover.go +++ b/decoder/hover.go @@ -12,7 +12,7 @@ import ( "github.com/zclconf/go-cty/cty" ) -func (d *Decoder) HoverAtPos(filename string, pos hcl.Pos) (*lang.HoverData, error) { +func (d *PathDecoder) HoverAtPos(filename string, pos hcl.Pos) (*lang.HoverData, error) { f, err := d.fileByName(filename) if err != nil { return nil, err @@ -23,14 +23,11 @@ func (d *Decoder) HoverAtPos(filename string, pos hcl.Pos) (*lang.HoverData, err return nil, err } - d.rootSchemaMu.RLock() - defer d.rootSchemaMu.RUnlock() - - if d.rootSchema == nil { + if d.pathCtx.Schema == nil { return nil, &NoSchemaError{} } - data, err := d.hoverAtPos(rootBody, d.rootSchema, pos) + data, err := d.hoverAtPos(rootBody, d.pathCtx.Schema, pos) if err != nil { return nil, err } @@ -38,7 +35,7 @@ func (d *Decoder) HoverAtPos(filename string, pos hcl.Pos) (*lang.HoverData, err return data, nil } -func (d *Decoder) hoverAtPos(body *hclsyntax.Body, bodySchema *schema.BodySchema, pos hcl.Pos) (*lang.HoverData, error) { +func (d *PathDecoder) hoverAtPos(body *hclsyntax.Body, bodySchema *schema.BodySchema, pos hcl.Pos) (*lang.HoverData, error) { if bodySchema == nil { return nil, nil } @@ -143,7 +140,7 @@ func (d *Decoder) hoverAtPos(body *hclsyntax.Body, bodySchema *schema.BodySchema } } -func (d *Decoder) hoverContentForLabel(i int, block *hclsyntax.Block, bSchema *schema.BlockSchema) lang.MarkupContent { +func (d *PathDecoder) hoverContentForLabel(i int, block *hclsyntax.Block, bSchema *schema.BlockSchema) lang.MarkupContent { value := block.Labels[i] labelSchema := bSchema.Labels[i] @@ -197,7 +194,7 @@ func hoverContentForAttribute(name string, schema *schema.AttributeSchema) lang. } } -func (d *Decoder) hoverContentForBlock(bType string, schema *schema.BlockSchema) lang.MarkupContent { +func (d *PathDecoder) hoverContentForBlock(bType string, schema *schema.BlockSchema) lang.MarkupContent { value := fmt.Sprintf("**%s** _%s_", bType, detailForBlock(schema)) if schema.Description.Value != "" { value += fmt.Sprintf("\n\n%s", schema.Description.Value) @@ -217,7 +214,7 @@ func (d *Decoder) hoverContentForBlock(bType string, schema *schema.BlockSchema) } } -func (d *Decoder) hoverDataForExpr(expr hcl.Expression, constraints ExprConstraints, nestingLvl int, pos hcl.Pos) (*lang.HoverData, error) { +func (d *PathDecoder) hoverDataForExpr(expr hcl.Expression, constraints ExprConstraints, nestingLvl int, pos hcl.Pos) (*lang.HoverData, error) { switch e := expr.(type) { case *hclsyntax.ScopeTraversalExpr: kw, ok := constraints.KeywordExpr() @@ -471,7 +468,7 @@ func (d *Decoder) hoverDataForExpr(expr hcl.Expression, constraints ExprConstrai return nil, fmt.Errorf("unsupported expression (%T)", expr) } -func (d *Decoder) hoverDataForObjectExpr(objExpr *hclsyntax.ObjectConsExpr, oe schema.ObjectExpr, nestingLvl int, pos hcl.Pos) (*lang.HoverData, error) { +func (d *PathDecoder) hoverDataForObjectExpr(objExpr *hclsyntax.ObjectConsExpr, oe schema.ObjectExpr, nestingLvl int, pos hcl.Pos) (*lang.HoverData, error) { declaredAttributes := make(map[string]hclsyntax.Expression, 0) for _, item := range objExpr.Items { key, _ := item.KeyExpr.Value(nil) @@ -595,12 +592,12 @@ func stringValFromTemplateExpr(tplExpr *hclsyntax.TemplateExpr) (cty.Value, bool return cty.StringVal(value), true } -func (d *Decoder) hoverContentForTraversalExpr(traversal hcl.Traversal, tes []schema.TraversalExpr) (string, error) { - if d.refTargetReader == nil { +func (d *PathDecoder) hoverContentForTraversalExpr(traversal hcl.Traversal, tes []schema.TraversalExpr) (string, error) { + if d.pathCtx.ReferenceTargets == nil { return "", &NoRefTargetFound{} } - allTargets := ReferenceTargets(d.refTargetReader()) + allTargets := ReferenceTargets(d.pathCtx.ReferenceTargets) origin, err := TraversalToReferenceOrigin(traversal, tes) if err != nil { diff --git a/decoder/hover_expressions_test.go b/decoder/hover_expressions_test.go index 5778c932..cc0ec115 100644 --- a/decoder/hover_expressions_test.go +++ b/decoder/hover_expressions_test.go @@ -1268,16 +1268,18 @@ _object_`), for i, tc := range testCases { t.Run(fmt.Sprintf("%d-%s", i, tc.name), func(t *testing.T) { - d := NewDecoder() - d.SetSchema(&schema.BodySchema{ + bodySchema := &schema.BodySchema{ Attributes: tc.attrSchema, - }) + } f, _ := hclsyntax.ParseConfig([]byte(tc.cfg), "test.tf", hcl.InitialPos) - err := d.LoadFile("test.tf", f) - if err != nil { - t.Fatal(err) - } + + d := testPathDecoder(t, &PathContext{ + Schema: bodySchema, + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) data, err := d.HoverAtPos("test.tf", tc.pos) @@ -1466,19 +1468,19 @@ func TestDecoder_HoverAtPos_traversalExpressions(t *testing.T) { for i, tc := range testCases { t.Run(fmt.Sprintf("%d-%s", i, tc.name), func(t *testing.T) { - d := NewDecoder() - d.SetSchema(&schema.BodySchema{ + bodySchema := &schema.BodySchema{ Attributes: tc.attrSchema, - }) - d.SetReferenceTargetReader(func() lang.ReferenceTargets { - return tc.refs - }) + } f, _ := hclsyntax.ParseConfig([]byte(tc.cfg), "test.tf", hcl.InitialPos) - err := d.LoadFile("test.tf", f) - if err != nil { - t.Fatal(err) - } + + d := testPathDecoder(t, &PathContext{ + Schema: bodySchema, + ReferenceTargets: tc.refs, + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) data, err := d.HoverAtPos("test.tf", tc.pos) if err != nil { diff --git a/decoder/hover_test.go b/decoder/hover_test.go index 1b38e3a5..cd03d914 100644 --- a/decoder/hover_test.go +++ b/decoder/hover_test.go @@ -17,17 +17,18 @@ import ( ) func TestDecoder_HoverAtPos_noSchema(t *testing.T) { - d := NewDecoder() f, pDiags := hclsyntax.ParseConfig(testConfig, "test.tf", hcl.InitialPos) if len(pDiags) > 0 { t.Fatal(pDiags) } - err := d.LoadFile("test.tf", f) - if err != nil { - t.Fatal(err) - } - _, err = d.HoverAtPos("test.tf", hcl.InitialPos) + d := testPathDecoder(t, &PathContext{ + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) + + _, err := d.HoverAtPos("test.tf", hcl.InitialPos) noSchemaErr := &NoSchemaError{} if !errors.As(err, &noSchemaErr) { t.Fatal("expected NoSchemaError for no schema") @@ -35,16 +36,17 @@ func TestDecoder_HoverAtPos_noSchema(t *testing.T) { } func TestDecoder_HoverAtPos_emptyBody(t *testing.T) { - d := NewDecoder() f := &hcl.File{ Body: hcl.EmptyBody(), } - err := d.LoadFile("test.tf", f) - if err != nil { - t.Fatal(err) - } - _, err = d.HoverAtPos("test.tf", hcl.InitialPos) + d := testPathDecoder(t, &PathContext{ + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) + + _, err := d.HoverAtPos("test.tf", hcl.InitialPos) unknownFormatErr := &UnknownFileFormatError{} if !errors.As(err, &unknownFormatErr) { t.Fatal("expected UnknownFileFormatError for empty body") @@ -52,7 +54,6 @@ func TestDecoder_HoverAtPos_emptyBody(t *testing.T) { } func TestDecoder_HoverAtPos_json(t *testing.T) { - d := NewDecoder() f, pDiags := json.Parse([]byte(`{ "customblock": { "label1": {} @@ -61,12 +62,14 @@ func TestDecoder_HoverAtPos_json(t *testing.T) { if len(pDiags) > 0 { t.Fatal(pDiags) } - err := d.LoadFile("test.tf.json", f) - if err != nil { - t.Fatal(err) - } - _, err = d.HoverAtPos("test.tf.json", hcl.InitialPos) + d := testPathDecoder(t, &PathContext{ + Files: map[string]*hcl.File{ + "test.tf.json": f, + }, + }) + + _, err := d.HoverAtPos("test.tf.json", hcl.InitialPos) unknownFormatErr := &UnknownFileFormatError{} if !errors.As(err, &unknownFormatErr) { t.Fatal("expected UnknownFileFormatError for JSON body") @@ -231,16 +234,17 @@ func TestDecoder_HoverAtPos_nilBodySchema(t *testing.T) { for i, tc := range testCases { t.Run(fmt.Sprintf("%d-%s", i, tc.name), func(t *testing.T) { - d := NewDecoder() - d.SetSchema(tc.rootSchema) f, pDiags := hclsyntax.ParseConfig([]byte(tc.config), "test.tf", hcl.InitialPos) if len(pDiags) > 0 { t.Fatal(pDiags) } - err := d.LoadFile("test.tf", f) - if err != nil { - t.Fatal(err) - } + + d := testPathDecoder(t, &PathContext{ + Schema: tc.rootSchema, + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) data, err := d.HoverAtPos("test.tf", tc.pos) if err != nil { @@ -273,8 +277,6 @@ func TestDecoder_HoverAtPos_unknownAttribute(t *testing.T) { }, } - d := NewDecoder() - d.SetSchema(bodySchema) f, pDiags := hclsyntax.ParseConfig([]byte(`resource "label1" "test" { blablah = 42 } @@ -282,12 +284,15 @@ func TestDecoder_HoverAtPos_unknownAttribute(t *testing.T) { if len(pDiags) > 0 { t.Fatal(pDiags) } - err := d.LoadFile("test.tf", f) - if err != nil { - t.Fatal(err) - } - _, err = d.HoverAtPos("test.tf", hcl.Pos{ + d := testPathDecoder(t, &PathContext{ + Schema: bodySchema, + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) + + _, err := d.HoverAtPos("test.tf", hcl.Pos{ Line: 2, Column: 6, Byte: 32, @@ -319,8 +324,6 @@ func TestDecoder_HoverAtPos_unknownBlock(t *testing.T) { }, } - d := NewDecoder() - d.SetSchema(bodySchema) f, pDiags := hclsyntax.ParseConfig([]byte(`customblock "label1" { } @@ -328,12 +331,15 @@ func TestDecoder_HoverAtPos_unknownBlock(t *testing.T) { if len(pDiags) > 0 { t.Fatal(pDiags) } - err := d.LoadFile("test.tf", f) - if err != nil { - t.Fatal(err) - } - _, err = d.HoverAtPos("test.tf", hcl.Pos{ + d := testPathDecoder(t, &PathContext{ + Schema: bodySchema, + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) + + _, err := d.HoverAtPos("test.tf", hcl.Pos{ Line: 2, Column: 1, Byte: 23, @@ -397,20 +403,21 @@ func TestDecoder_HoverAtPos_invalidBlockPositions(t *testing.T) { }, } - d := NewDecoder() - d.SetSchema(bodySchema) f, pDiags := hclsyntax.ParseConfig(testConfig, "test.tf", hcl.InitialPos) if len(pDiags) > 0 { t.Fatal(pDiags) } - err := d.LoadFile("test.tf", f) - if err != nil { - t.Fatal(err) - } + + d := testPathDecoder(t, &PathContext{ + Schema: bodySchema, + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) for i, tc := range testCases { t.Run(fmt.Sprintf("%d-%s", i, tc.name), func(t *testing.T) { - _, err = d.HoverAtPos("test.tf", tc.pos) + _, err := d.HoverAtPos("test.tf", tc.pos) if err == nil { t.Fatal("expected error") } @@ -444,13 +451,14 @@ func TestDecoder_HoverAtPos_rightHandSide(t *testing.T) { } `) - d := NewDecoder() - d.SetSchema(bodySchema) f, _ := hclsyntax.ParseConfig(testConfig, "test.tf", hcl.InitialPos) - err := d.LoadFile("test.tf", f) - if err != nil { - t.Fatal(err) - } + + d := testPathDecoder(t, &PathContext{ + Schema: bodySchema, + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) data, err := d.HoverAtPos("test.tf", hcl.Pos{ Line: 2, @@ -537,14 +545,15 @@ func TestDecoder_HoverAtPos_basic(t *testing.T) { } } `) - d := NewDecoder() - d.SetSchema(bodySchema) f, _ := hclsyntax.ParseConfig(testConfig, "test.tf", hcl.InitialPos) - err := d.LoadFile("test.tf", f) - if err != nil { - t.Fatal(err) - } + + d := testPathDecoder(t, &PathContext{ + Schema: bodySchema, + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) testCases := []struct { name string @@ -792,14 +801,14 @@ My food block } for i, tc := range testCases { t.Run(fmt.Sprintf("%d-%s", i, tc.name), func(t *testing.T) { - d := NewDecoder() - d.SetSchema(bodySchema) - f, _ := hclsyntax.ParseConfig([]byte(tc.cfg), "test.tf", hcl.InitialPos) - err := d.LoadFile("test.tf", f) - if err != nil { - t.Fatal(err) - } + + d := testPathDecoder(t, &PathContext{ + Schema: bodySchema, + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) data, err := d.HoverAtPos("test.tf", tc.pos) if err != nil { @@ -835,9 +844,6 @@ func TestDecoder_HoverAtPos_typeDeclaration(t *testing.T) { }, } - d := NewDecoder() - d.SetSchema(bodySchema) - testCases := []struct { name string cfg string @@ -896,10 +902,14 @@ func TestDecoder_HoverAtPos_typeDeclaration(t *testing.T) { t.Run(fmt.Sprintf("%d-%s", i, tc.name), func(t *testing.T) { testConfig := []byte(tc.cfg) f, _ := hclsyntax.ParseConfig(testConfig, "test.tf", hcl.InitialPos) - err := d.LoadFile("test.tf", f) - if err != nil { - t.Fatal(err) - } + + d := testPathDecoder(t, &PathContext{ + Schema: bodySchema, + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) + pos := hcl.Pos{Line: 2, Column: 6, Byte: 32} data, err := d.HoverAtPos("test.tf", pos) if err != nil { diff --git a/decoder/label_candidates.go b/decoder/label_candidates.go index bdf25e86..75d9b27b 100644 --- a/decoder/label_candidates.go +++ b/decoder/label_candidates.go @@ -12,7 +12,7 @@ import ( "github.com/hashicorp/hcl/v2/hclsyntax" ) -func (d *Decoder) labelCandidatesFromDependentSchema(idx int, db map[schema.SchemaKey]*schema.BodySchema, prefixRng, editRng hcl.Range, block *hclsyntax.Block, labelSchemas []*schema.LabelSchema) (lang.Candidates, error) { +func (d *PathDecoder) labelCandidatesFromDependentSchema(idx int, db map[schema.SchemaKey]*schema.BodySchema, prefixRng, editRng hcl.Range, block *hclsyntax.Block, labelSchemas []*schema.LabelSchema) (lang.Candidates, error) { candidates := lang.NewCandidates() candidates.IsComplete = true count := 0 diff --git a/decoder/label_candidates_test.go b/decoder/label_candidates_test.go index 00cc078e..bb12e270 100644 --- a/decoder/label_candidates_test.go +++ b/decoder/label_candidates_test.go @@ -52,17 +52,17 @@ func TestDecoder_CandidateAtPos_incompleteLabels(t *testing.T) { }, } - d := NewDecoder() - d.maxCandidates = 1 - d.SetSchema(bodySchema) - f, _ := hclsyntax.ParseConfig([]byte(`customblock "" { } `), "test.tf", hcl.InitialPos) - err := d.LoadFile("test.tf", f) - if err != nil { - t.Fatal(err) - } + + d := testPathDecoder(t, &PathContext{ + Schema: bodySchema, + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) + d.maxCandidates = 1 candidates, err := d.CandidatesAtPos("test.tf", hcl.Pos{ Line: 1, @@ -605,14 +605,14 @@ func TestCandidatesAtPos_prefillRequiredFields(t *testing.T) { t.Run(tt.name, func(t *testing.T) { f, _ := hclsyntax.ParseConfig([]byte(startingConfig), "test.tf", hcl.InitialPos) - d := NewDecoder() + d := testPathDecoder(t, &PathContext{ + Schema: tt.schema, + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) + d.maxCandidates = 1 d.PrefillRequiredFields = tt.prefill - d.SetSchema(tt.schema) - - err := d.LoadFile("test.tf", f) - if err != nil { - t.Fatal(err) - } got, err := d.CandidatesAtPos("test.tf", startingPos) if err != nil { diff --git a/decoder/links.go b/decoder/links.go index 92845b8b..fa2941f4 100644 --- a/decoder/links.go +++ b/decoder/links.go @@ -12,7 +12,7 @@ import ( // LinksInFile returns links relevant to parts of config in the given file // // A link (URI) typically points to the documentation. -func (d *Decoder) LinksInFile(filename string) ([]lang.Link, error) { +func (d *PathDecoder) LinksInFile(filename string) ([]lang.Link, error) { f, err := d.fileByName(filename) if err != nil { return nil, err @@ -23,17 +23,14 @@ func (d *Decoder) LinksInFile(filename string) ([]lang.Link, error) { return nil, err } - d.rootSchemaMu.RLock() - defer d.rootSchemaMu.RUnlock() - - if d.rootSchema == nil { + if d.pathCtx.Schema == nil { return []lang.Link{}, &NoSchemaError{} } - return d.linksInBody(body, d.rootSchema) + return d.linksInBody(body, d.pathCtx.Schema) } -func (d *Decoder) linksInBody(body *hclsyntax.Body, bodySchema *schema.BodySchema) ([]lang.Link, error) { +func (d *PathDecoder) linksInBody(body *hclsyntax.Body, bodySchema *schema.BodySchema) ([]lang.Link, error) { links := make([]lang.Link, 0) for _, block := range body.Blocks { @@ -66,20 +63,20 @@ func (d *Decoder) linksInBody(body *hclsyntax.Body, bodySchema *schema.BodySchem return links, nil } -func (d *Decoder) docsURL(uri, utmContent string) (*url.URL, error) { +func (d *PathDecoder) docsURL(uri, utmContent string) (*url.URL, error) { u, err := url.Parse(uri) if err != nil { return nil, err } q := u.Query() - if d.utmSource != "" { - q.Set("utm_source", d.utmSource) + if d.decoderCtx.UtmSource != "" { + q.Set("utm_source", d.decoderCtx.UtmSource) } - if d.utmMedium != "" { - q.Set("utm_medium", d.utmMedium) + if d.decoderCtx.UtmMedium != "" { + q.Set("utm_medium", d.decoderCtx.UtmMedium) } - if d.useUtmContent { + if d.decoderCtx.UseUtmContent { q.Set("utm_content", utmContent) } u.RawQuery = q.Encode() diff --git a/decoder/links_test.go b/decoder/links_test.go index c7bde711..fe8f253c 100644 --- a/decoder/links_test.go +++ b/decoder/links_test.go @@ -59,17 +59,17 @@ func TestLinksInFile(t *testing.T) { } `) - d := NewDecoder() f, pDiags := hclsyntax.ParseConfig(testConfig, "test.tf", hcl.InitialPos) if len(pDiags) > 0 { t.Fatal(pDiags) } - err := d.LoadFile("test.tf", f) - if err != nil { - t.Fatal(err) - } - d.SetSchema(bodySchema) + d := testPathDecoder(t, &PathContext{ + Schema: bodySchema, + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) links, err := d.LinksInFile("test.tf") if err != nil { @@ -102,7 +102,6 @@ func TestLinksInFile(t *testing.T) { } func TestLinksInFile_json(t *testing.T) { - d := NewDecoder() f, pDiags := json.Parse([]byte(`{ "customblock": { "label1": {} @@ -111,12 +110,14 @@ func TestLinksInFile_json(t *testing.T) { if len(pDiags) > 0 { t.Fatal(pDiags) } - err := d.LoadFile("test.tf.json", f) - if err != nil { - t.Fatal(err) - } - _, err = d.LinksInFile("test.tf.json") + d := testPathDecoder(t, &PathContext{ + Files: map[string]*hcl.File{ + "test.tf.json": f, + }, + }) + + _, err := d.LinksInFile("test.tf.json") unknownFormatErr := &UnknownFileFormatError{} if !errors.As(err, &unknownFormatErr) { t.Fatal("expected UnknownFileFormatError for JSON body") diff --git a/decoder/path_context.go b/decoder/path_context.go new file mode 100644 index 00000000..42c2e847 --- /dev/null +++ b/decoder/path_context.go @@ -0,0 +1,15 @@ +package decoder + +import ( + "github.com/hashicorp/hcl-lang/lang" + "github.com/hashicorp/hcl-lang/schema" + "github.com/hashicorp/hcl/v2" +) + +type PathContext struct { + DirPath string + Schema *schema.BodySchema + ReferenceOrigins lang.ReferenceOrigins + ReferenceTargets lang.ReferenceTargets + Files map[string]*hcl.File +} diff --git a/decoder/path_decoder.go b/decoder/path_decoder.go new file mode 100644 index 00000000..68756104 --- /dev/null +++ b/decoder/path_decoder.go @@ -0,0 +1,88 @@ +package decoder + +import ( + "context" + "sort" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" +) + +type PathDecoder struct { + pathCtx *PathContext + decoderCtx DecoderContext + + // maxCandidates defines maximum number of completion candidates returned + maxCandidates uint + + // PrefillRequiredFields enriches label-based completion candidates + // with required attributes and blocks + PrefillRequiredFields bool +} + +func (d *Decoder) Path(ctx context.Context, path string) *PathDecoder { + return &PathDecoder{ + pathCtx: d.pathReader.PathContext(ctx, path), + decoderCtx: d.ctx, + maxCandidates: 100, + } +} + +func (d *PathDecoder) bytesForFile(file string) ([]byte, error) { + f, ok := d.pathCtx.Files[file] + if !ok { + return nil, &FileNotFoundError{Filename: file} + } + + return f.Bytes, nil +} + +// filenames returns a slice of filenames already loaded via LoadFile +func (d *PathDecoder) filenames() []string { + var files []string + + for filename := range d.pathCtx.Files { + files = append(files, filename) + } + + sort.Strings(files) + + return files +} + +func (d *PathDecoder) bytesFromRange(rng hcl.Range) ([]byte, error) { + b, err := d.bytesForFile(rng.Filename) + if err != nil { + return nil, err + } + + return rng.SliceBytes(b), nil +} + +func (d *PathDecoder) fileByName(name string) (*hcl.File, error) { + f, ok := d.pathCtx.Files[name] + if !ok { + return nil, &FileNotFoundError{Filename: name} + } + return f, nil +} + +func (d *PathDecoder) bodyForFileAndPos(name string, f *hcl.File, pos hcl.Pos) (*hclsyntax.Body, error) { + body, isHcl := f.Body.(*hclsyntax.Body) + if !isHcl { + return nil, &UnknownFileFormatError{Filename: name} + } + + if !body.Range().ContainsPos(pos) && + !posEqual(body.Range().Start, pos) && + !posEqual(body.Range().End, pos) { + + return nil, &PosOutOfRangeError{ + Filename: name, + Pos: pos, + Range: body.Range(), + } + } + + return body, nil +} diff --git a/decoder/reference_origins.go b/decoder/reference_origins.go index 19921532..57eddee5 100644 --- a/decoder/reference_origins.go +++ b/decoder/reference_origins.go @@ -12,7 +12,7 @@ import ( // ReferenceOriginAtPos returns the ReferenceOrigin // enclosing the position in a file, if one exists, else nil -func (d *Decoder) ReferenceOriginAtPos(filename string, pos hcl.Pos) (*lang.ReferenceOrigin, error) { +func (d *PathDecoder) ReferenceOriginAtPos(filename string, pos hcl.Pos) (*lang.ReferenceOrigin, error) { // TODO: Filter d.refOriginReader instead here f, err := d.fileByName(filename) @@ -25,37 +25,32 @@ func (d *Decoder) ReferenceOriginAtPos(filename string, pos hcl.Pos) (*lang.Refe return nil, err } - d.rootSchemaMu.RLock() - defer d.rootSchemaMu.RUnlock() - if d.rootSchema == nil { + if d.pathCtx.Schema == nil { return nil, &NoSchemaError{} } - return d.referenceOriginAtPos(rootBody, d.rootSchema, pos) + return d.referenceOriginAtPos(rootBody, d.pathCtx.Schema, pos) } -func (d *Decoder) ReferenceOriginsTargeting(refTarget lang.ReferenceTarget) (lang.ReferenceOrigins, error) { - if d.refOriginReader == nil { +func (d *PathDecoder) ReferenceOriginsTargeting(refTarget lang.ReferenceTarget) (lang.ReferenceOrigins, error) { + if d.pathCtx.ReferenceOrigins == nil { return nil, nil } - allOrigins := ReferenceOrigins(d.refOriginReader()) + allOrigins := ReferenceOrigins(d.pathCtx.ReferenceOrigins) return allOrigins.Targeting(refTarget), nil } -func (d *Decoder) CollectReferenceOrigins() (lang.ReferenceOrigins, error) { +func (d *PathDecoder) CollectReferenceOrigins() (lang.ReferenceOrigins, error) { refOrigins := make(lang.ReferenceOrigins, 0) - d.rootSchemaMu.RLock() - defer d.rootSchemaMu.RUnlock() - - if d.rootSchema == nil { + if d.pathCtx.Schema == nil { // unable to collect reference origins without schema return refOrigins, &NoSchemaError{} } - files := d.Filenames() + files := d.filenames() for _, filename := range files { f, err := d.fileByName(filename) if err != nil { @@ -63,7 +58,7 @@ func (d *Decoder) CollectReferenceOrigins() (lang.ReferenceOrigins, error) { continue } - refOrigins = append(refOrigins, d.referenceOriginsInBody(f.Body, d.rootSchema)...) + refOrigins = append(refOrigins, d.referenceOriginsInBody(f.Body, d.pathCtx.Schema)...) } sort.SliceStable(refOrigins, func(i, j int) bool { @@ -74,7 +69,7 @@ func (d *Decoder) CollectReferenceOrigins() (lang.ReferenceOrigins, error) { return refOrigins, nil } -func (d *Decoder) referenceOriginsInBody(body hcl.Body, bodySchema *schema.BodySchema) lang.ReferenceOrigins { +func (d *PathDecoder) referenceOriginsInBody(body hcl.Body, bodySchema *schema.BodySchema) lang.ReferenceOrigins { origins := make(lang.ReferenceOrigins, 0) if bodySchema == nil { @@ -114,7 +109,7 @@ func (d *Decoder) referenceOriginsInBody(body hcl.Body, bodySchema *schema.BodyS return origins } -func (d *Decoder) findOriginsInExpression(expr hcl.Expression, ec schema.ExprConstraints) lang.ReferenceOrigins { +func (d *PathDecoder) findOriginsInExpression(expr hcl.Expression, ec schema.ExprConstraints) lang.ReferenceOrigins { origins := make(lang.ReferenceOrigins, 0) switch eType := expr.(type) { @@ -232,7 +227,7 @@ func traversalsToReferenceOrigins(traversals []hcl.Traversal, tes schema.Travers return origins } -func (d *Decoder) referenceOriginAtPos(body *hclsyntax.Body, bodySchema *schema.BodySchema, pos hcl.Pos) (*lang.ReferenceOrigin, error) { +func (d *PathDecoder) referenceOriginAtPos(body *hclsyntax.Body, bodySchema *schema.BodySchema, pos hcl.Pos) (*lang.ReferenceOrigin, error) { for _, attr := range body.Attributes { if d.isPosInsideAttrExpr(attr, pos) { aSchema, ok := bodySchema.Attributes[attr.Name] @@ -276,7 +271,7 @@ func (d *Decoder) referenceOriginAtPos(body *hclsyntax.Body, bodySchema *schema. return nil, nil } -func (d *Decoder) traversalAtPos(expr hclsyntax.Expression, pos hcl.Pos) (hcl.Traversal, bool) { +func (d *PathDecoder) traversalAtPos(expr hclsyntax.Expression, pos hcl.Pos) (hcl.Traversal, bool) { for _, traversal := range expr.Variables() { if traversal.SourceRange().ContainsPos(pos) { return traversal, true diff --git a/decoder/reference_origins_collect_hcl_test.go b/decoder/reference_origins_collect_hcl_test.go index cf50b04a..99ca5c77 100644 --- a/decoder/reference_origins_collect_hcl_test.go +++ b/decoder/reference_origins_collect_hcl_test.go @@ -943,14 +943,14 @@ tup = [ var.three ] } for i, tc := range testCases { t.Run(fmt.Sprintf("%d/%s", i, tc.name), func(t *testing.T) { - d := NewDecoder() - d.SetSchema(tc.schema) - f, _ := hclsyntax.ParseConfig([]byte(tc.cfg), "test.tf", hcl.InitialPos) - err := d.LoadFile("test.tf", f) - if err != nil { - t.Fatal(err) - } + + d := testPathDecoder(t, &PathContext{ + Schema: tc.schema, + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) origins, err := d.CollectReferenceOrigins() if err != nil { diff --git a/decoder/reference_origins_collect_json_test.go b/decoder/reference_origins_collect_json_test.go index 5b252342..af96e72c 100644 --- a/decoder/reference_origins_collect_json_test.go +++ b/decoder/reference_origins_collect_json_test.go @@ -919,17 +919,17 @@ func TestCollectReferenceOrigins_json(t *testing.T) { } for i, tc := range testCases { t.Run(fmt.Sprintf("%d/%s", i, tc.name), func(t *testing.T) { - d := NewDecoder() - d.SetSchema(tc.schema) - f, diags := json.Parse([]byte(tc.cfg), "test.tf.json") if len(diags) > 0 { t.Fatal(diags) } - err := d.LoadFile("test.tf.json", f) - if err != nil { - t.Fatal(err) - } + + d := testPathDecoder(t, &PathContext{ + Schema: tc.schema, + Files: map[string]*hcl.File{ + "test.tf.json": f, + }, + }) origins, err := d.CollectReferenceOrigins() if err != nil { diff --git a/decoder/reference_origins_test.go b/decoder/reference_origins_test.go index fb0dbad8..1e098fb4 100644 --- a/decoder/reference_origins_test.go +++ b/decoder/reference_origins_test.go @@ -314,14 +314,14 @@ func TestReferenceOriginAtPos(t *testing.T) { for i, tc := range testCases { t.Run(fmt.Sprintf("%d-%s", i, tc.name), func(t *testing.T) { - d := NewDecoder() - d.SetSchema(tc.bodySchema) - f, _ := hclsyntax.ParseConfig([]byte(tc.cfg), "test.tf", hcl.InitialPos) - err := d.LoadFile("test.tf", f) - if err != nil { - t.Fatal(err) - } + + d := testPathDecoder(t, &PathContext{ + Schema: tc.bodySchema, + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) refOrigin, err := d.ReferenceOriginAtPos("test.tf", tc.pos) if err != nil { @@ -336,18 +336,18 @@ func TestReferenceOriginAtPos(t *testing.T) { } func TestReferenceOriginAtPos_json(t *testing.T) { - d := NewDecoder() - f, diags := json.Parse([]byte(`{}`), "test.tf.json") if len(diags) > 0 { t.Fatal(diags) } - err := d.LoadFile("test.tf.json", f) - if err != nil { - t.Fatal(err) - } - _, err = d.ReferenceOriginAtPos("test.tf.json", hcl.InitialPos) + d := testPathDecoder(t, &PathContext{ + Files: map[string]*hcl.File{ + "test.tf.json": f, + }, + }) + + _, err := d.ReferenceOriginAtPos("test.tf.json", hcl.InitialPos) unknownFormatErr := &UnknownFileFormatError{} if !errors.As(err, &unknownFormatErr) { t.Fatal("expected UnknownFileFormatError for JSON body") @@ -616,10 +616,10 @@ func TestReferenceOriginsTargeting(t *testing.T) { } for i, tc := range testCases { t.Run(fmt.Sprintf("%d-%s", i, tc.name), func(t *testing.T) { - d := NewDecoder() - d.SetReferenceOriginReader(func() lang.ReferenceOrigins { - return tc.allOrigins + d := testPathDecoder(t, &PathContext{ + ReferenceOrigins: tc.allOrigins, }) + origins, err := d.ReferenceOriginsTargeting(tc.refTarget) if err != nil { t.Fatal(err) diff --git a/decoder/reference_targets.go b/decoder/reference_targets.go index ae1d92f1..f3fa9534 100644 --- a/decoder/reference_targets.go +++ b/decoder/reference_targets.go @@ -17,12 +17,12 @@ import ( // ReferenceTargetForOrigin returns the first ReferenceTarget // with matching ReferenceOrigin Address, if one exists, else nil -func (d *Decoder) ReferenceTargetForOrigin(refOrigin lang.ReferenceOrigin) (*lang.ReferenceTarget, error) { - if d.refTargetReader == nil { +func (d *PathDecoder) ReferenceTargetForOrigin(refOrigin lang.ReferenceOrigin) (*lang.ReferenceTarget, error) { + if d.pathCtx.ReferenceTargets == nil { return nil, nil } - allTargets := ReferenceTargets(d.refTargetReader()) + allTargets := ReferenceTargets(d.pathCtx.ReferenceTargets) ref, err := allTargets.FirstTargetableBy(refOrigin) if err != nil { @@ -35,12 +35,12 @@ func (d *Decoder) ReferenceTargetForOrigin(refOrigin lang.ReferenceOrigin) (*lan return &ref, nil } -func (d *Decoder) ReferenceTargetsInFile(file string) (lang.ReferenceTargets, error) { - if d.refTargetReader == nil { +func (d *PathDecoder) ReferenceTargetsInFile(file string) (lang.ReferenceTargets, error) { + if d.pathCtx.ReferenceTargets == nil { return nil, nil } - allTargets := ReferenceTargets(d.refTargetReader()) + allTargets := ReferenceTargets(d.pathCtx.ReferenceTargets) targets := make(lang.ReferenceTargets, 0) @@ -62,12 +62,12 @@ func (d *Decoder) ReferenceTargetsInFile(file string) (lang.ReferenceTargets, er return targets, nil } -func (d *Decoder) OutermostReferenceTargetsAtPos(file string, pos hcl.Pos) (lang.ReferenceTargets, error) { - if d.refTargetReader == nil { +func (d *PathDecoder) OutermostReferenceTargetsAtPos(file string, pos hcl.Pos) (lang.ReferenceTargets, error) { + if d.pathCtx.ReferenceTargets == nil { return nil, nil } - allTargets := ReferenceTargets(d.refTargetReader()) + allTargets := ReferenceTargets(d.pathCtx.ReferenceTargets) matchingTargets := make(lang.ReferenceTargets, 0) for _, target := range allTargets { @@ -85,17 +85,17 @@ func (d *Decoder) OutermostReferenceTargetsAtPos(file string, pos hcl.Pos) (lang return matchingTargets, nil } -func (d *Decoder) InnermostReferenceTargetsAtPos(file string, pos hcl.Pos) (lang.ReferenceTargets, error) { - if d.refTargetReader == nil { +func (d *PathDecoder) InnermostReferenceTargetsAtPos(file string, pos hcl.Pos) (lang.ReferenceTargets, error) { + if d.pathCtx.ReferenceTargets == nil { return nil, nil } - targets, _ := d.innermostReferenceTargetsAtPos(d.refTargetReader(), file, pos) + targets, _ := d.innermostReferenceTargetsAtPos(d.pathCtx.ReferenceTargets, file, pos) return targets, nil } -func (d *Decoder) innermostReferenceTargetsAtPos(targets lang.ReferenceTargets, file string, pos hcl.Pos) (lang.ReferenceTargets, bool) { +func (d *PathDecoder) innermostReferenceTargetsAtPos(targets lang.ReferenceTargets, file string, pos hcl.Pos) (lang.ReferenceTargets, bool) { allTargets := ReferenceTargets(targets) matchingTargets := make(lang.ReferenceTargets, 0) @@ -300,29 +300,27 @@ func (a Address) FirstSteps(steps uint) Address { return a[0:steps] } -func (d *Decoder) CollectReferenceTargets() (lang.ReferenceTargets, error) { - d.rootSchemaMu.RLock() - defer d.rootSchemaMu.RUnlock() - if d.rootSchema == nil { +func (d *PathDecoder) CollectReferenceTargets() (lang.ReferenceTargets, error) { + if d.pathCtx.Schema == nil { // unable to collect reference targets without schema return nil, &NoSchemaError{} } refs := make(lang.ReferenceTargets, 0) - files := d.Filenames() + files := d.filenames() for _, filename := range files { f, err := d.fileByName(filename) if err != nil { // skip unparseable file continue } - refs = append(refs, d.decodeReferenceTargetsForBody(f.Body, nil, d.rootSchema)...) + refs = append(refs, d.decodeReferenceTargetsForBody(f.Body, nil, d.pathCtx.Schema)...) } return refs, nil } -func (d *Decoder) decodeReferenceTargetsForBody(body hcl.Body, parentBlock *blockContent, bodySchema *schema.BodySchema) lang.ReferenceTargets { +func (d *PathDecoder) decodeReferenceTargetsForBody(body hcl.Body, parentBlock *blockContent, bodySchema *schema.BodySchema) lang.ReferenceTargets { refs := make(lang.ReferenceTargets, 0) if bodySchema == nil { @@ -865,7 +863,7 @@ func bodySchemaAsAttrTypes(bodySchema *schema.BodySchema) map[string]cty.Type { return attrTypes } -func (d *Decoder) collectInferredReferenceTargetsForBody(addr lang.Address, scopeId lang.ScopeId, body hcl.Body, bodySchema *schema.BodySchema) lang.ReferenceTargets { +func (d *PathDecoder) collectInferredReferenceTargetsForBody(addr lang.Address, scopeId lang.ScopeId, body hcl.Body, bodySchema *schema.BodySchema) lang.ReferenceTargets { refs := make(lang.ReferenceTargets, 0) content := decodeBody(body, bodySchema) @@ -1109,7 +1107,7 @@ func blocksTypesWithSchema(body hcl.Body, bodySchema *schema.BodySchema) blockTy return blockTypes } -func (d *Decoder) bytesInRange(rng hcl.Range) ([]byte, error) { +func (d *PathDecoder) bytesInRange(rng hcl.Range) ([]byte, error) { f, err := d.fileByName(rng.Filename) if err != nil { return nil, err diff --git a/decoder/reference_targets_collect_hcl_test.go b/decoder/reference_targets_collect_hcl_test.go index 779dd871..4eba3461 100644 --- a/decoder/reference_targets_collect_hcl_test.go +++ b/decoder/reference_targets_collect_hcl_test.go @@ -4722,14 +4722,14 @@ module "different" { for i, tc := range testCases { t.Run(fmt.Sprintf("%d-%s", i, tc.name), func(t *testing.T) { - d := NewDecoder() - d.SetSchema(tc.schema) - f, _ := hclsyntax.ParseConfig([]byte(tc.cfg), "test.tf", hcl.InitialPos) - err := d.LoadFile("test.tf", f) - if err != nil { - t.Fatal(err) - } + + d := testPathDecoder(t, &PathContext{ + Schema: tc.schema, + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) refs, err := d.CollectReferenceTargets() if err != nil { diff --git a/decoder/reference_targets_collect_json_test.go b/decoder/reference_targets_collect_json_test.go index 55b5234f..229caa38 100644 --- a/decoder/reference_targets_collect_json_test.go +++ b/decoder/reference_targets_collect_json_test.go @@ -4146,17 +4146,17 @@ func TestCollectReferenceTargets_json(t *testing.T) { for i, tc := range testCases { t.Run(fmt.Sprintf("%d-%s", i, tc.name), func(t *testing.T) { - d := NewDecoder() - d.SetSchema(tc.schema) - f, diags := json.Parse([]byte(tc.cfg), "test.tf.json") if len(diags) > 0 { t.Fatal(diags) } - err := d.LoadFile("test.tf.json", f) - if err != nil { - t.Fatal(err) - } + + d := testPathDecoder(t, &PathContext{ + Schema: tc.schema, + Files: map[string]*hcl.File{ + "test.tf.json": f, + }, + }) refs, err := d.CollectReferenceTargets() if err != nil { diff --git a/decoder/reference_targets_test.go b/decoder/reference_targets_test.go index df89b326..bfff2567 100644 --- a/decoder/reference_targets_test.go +++ b/decoder/reference_targets_test.go @@ -88,7 +88,7 @@ func TestAddress_Equals_stringIndexStep(t *testing.T) { } func TestCollectReferenceTargets_noSchema(t *testing.T) { - d := NewDecoder() + d := testPathDecoder(t, &PathContext{}) _, err := d.CollectReferenceTargets() if err == nil { t.Fatal("expected error when no schema is set") @@ -270,9 +270,8 @@ func TestReferenceTargetForOrigin(t *testing.T) { } for i, tc := range testCases { t.Run(fmt.Sprintf("%d-%s", i, tc.name), func(t *testing.T) { - d := NewDecoder() - d.SetReferenceTargetReader(func() lang.ReferenceTargets { - return tc.refTargets + d := testPathDecoder(t, &PathContext{ + ReferenceTargets: tc.refTargets, }) refTarget, err := d.ReferenceTargetForOrigin(tc.refOrigin) @@ -578,9 +577,8 @@ func TestOutermostReferenceTargetsAtPos(t *testing.T) { } for i, tc := range testCases { t.Run(fmt.Sprintf("%d-%s", i, tc.name), func(t *testing.T) { - d := NewDecoder() - d.SetReferenceTargetReader(func() lang.ReferenceTargets { - return tc.refTargets + d := testPathDecoder(t, &PathContext{ + ReferenceTargets: tc.refTargets, }) refTargets, err := d.OutermostReferenceTargetsAtPos(tc.filename, tc.pos) @@ -971,9 +969,8 @@ func TestInnermostReferenceTargetsAtPos(t *testing.T) { } for i, tc := range testCases { t.Run(fmt.Sprintf("%d-%s", i, tc.name), func(t *testing.T) { - d := NewDecoder() - d.SetReferenceTargetReader(func() lang.ReferenceTargets { - return tc.refTargets + d := testPathDecoder(t, &PathContext{ + ReferenceTargets: tc.refTargets, }) refTargets, err := d.InnermostReferenceTargetsAtPos(tc.filename, tc.pos) @@ -1095,9 +1092,8 @@ func TestReferenceTargetsInFile(t *testing.T) { } for i, tc := range testCases { t.Run(fmt.Sprintf("%d-%s", i, tc.name), func(t *testing.T) { - d := NewDecoder() - d.SetReferenceTargetReader(func() lang.ReferenceTargets { - return tc.refTargets + d := testPathDecoder(t, &PathContext{ + ReferenceTargets: tc.refTargets, }) targets, err := d.ReferenceTargetsInFile(tc.filename) diff --git a/decoder/semantic_tokens.go b/decoder/semantic_tokens.go index f36386a3..afd4a5c3 100644 --- a/decoder/semantic_tokens.go +++ b/decoder/semantic_tokens.go @@ -12,7 +12,7 @@ import ( // SemanticTokensInFile returns a sequence of semantic tokens // within the config file. -func (d *Decoder) SemanticTokensInFile(filename string) ([]lang.SemanticToken, error) { +func (d *PathDecoder) SemanticTokensInFile(filename string) ([]lang.SemanticToken, error) { f, err := d.fileByName(filename) if err != nil { return nil, err @@ -23,11 +23,11 @@ func (d *Decoder) SemanticTokensInFile(filename string) ([]lang.SemanticToken, e return nil, err } - if d.rootSchema == nil { + if d.pathCtx.Schema == nil { return []lang.SemanticToken{}, nil } - tokens := d.tokensForBody(body, d.rootSchema, false) + tokens := d.tokensForBody(body, d.pathCtx.Schema, false) sort.Slice(tokens, func(i, j int) bool { return tokens[i].Range.Start.Byte < tokens[j].Range.Start.Byte @@ -36,7 +36,7 @@ func (d *Decoder) SemanticTokensInFile(filename string) ([]lang.SemanticToken, e return tokens, nil } -func (d *Decoder) tokensForBody(body *hclsyntax.Body, bodySchema *schema.BodySchema, isDependent bool) []lang.SemanticToken { +func (d *PathDecoder) tokensForBody(body *hclsyntax.Body, bodySchema *schema.BodySchema, isDependent bool) []lang.SemanticToken { tokens := make([]lang.SemanticToken, 0) if bodySchema == nil { @@ -125,7 +125,7 @@ func (d *Decoder) tokensForBody(body *hclsyntax.Body, bodySchema *schema.BodySch return tokens } -func (d *Decoder) tokensForExpression(expr hclsyntax.Expression, constraints ExprConstraints) []lang.SemanticToken { +func (d *PathDecoder) tokensForExpression(expr hclsyntax.Expression, constraints ExprConstraints) []lang.SemanticToken { tokens := make([]lang.SemanticToken, 0) switch eType := expr.(type) { @@ -143,8 +143,8 @@ func (d *Decoder) tokensForExpression(expr hclsyntax.Expression, constraints Exp } tes, ok := constraints.TraversalExprs() - if ok && d.refTargetReader != nil { - refs := ReferenceTargets(d.refTargetReader()) + if ok && d.pathCtx.ReferenceTargets != nil { + refs := ReferenceTargets(d.pathCtx.ReferenceTargets) traversal := eType.AsTraversal() origin, err := TraversalToReferenceOrigin(traversal, tes) @@ -365,7 +365,7 @@ func (d *Decoder) tokensForExpression(expr hclsyntax.Expression, constraints Exp return tokens } -func (d *Decoder) tokensForObjectConsTypeDeclarationExpr(expr *hclsyntax.ObjectConsExpr, constraints ExprConstraints) []lang.SemanticToken { +func (d *PathDecoder) tokensForObjectConsTypeDeclarationExpr(expr *hclsyntax.ObjectConsExpr, constraints ExprConstraints) []lang.SemanticToken { tokens := make([]lang.SemanticToken, 0) for _, item := range expr.Items { key, _ := item.KeyExpr.Value(nil) diff --git a/decoder/semantic_tokens_expr_test.go b/decoder/semantic_tokens_expr_test.go index 4de35bb5..a9cd7711 100644 --- a/decoder/semantic_tokens_expr_test.go +++ b/decoder/semantic_tokens_expr_test.go @@ -1434,19 +1434,21 @@ EOT for i, tc := range testCases { t.Run(fmt.Sprintf("%d-%s", i, tc.name), func(t *testing.T) { - d := NewDecoder() - d.SetSchema(&schema.BodySchema{ + bodySchema := &schema.BodySchema{ Attributes: tc.attrSchema, - }) + } f, pDiags := hclsyntax.ParseConfig([]byte(tc.cfg), "test.tf", hcl.InitialPos) if len(pDiags) > 0 { t.Fatal(pDiags) } - err := d.LoadFile("test.tf", f) - if err != nil { - t.Fatal(err) - } + + d := testPathDecoder(t, &PathContext{ + Schema: bodySchema, + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) tokens, err := d.SemanticTokensInFile("test.tf") if err != nil { @@ -1911,22 +1913,22 @@ func TestDecoder_SemanticTokensInFile_traversalExpression(t *testing.T) { for i, tc := range testCases { t.Run(fmt.Sprintf("%d-%s", i, tc.name), func(t *testing.T) { - d := NewDecoder() - d.SetSchema(&schema.BodySchema{ + bodySchema := &schema.BodySchema{ Attributes: tc.attrSchema, - }) - d.SetReferenceTargetReader(func() lang.ReferenceTargets { - return tc.refs - }) + } f, pDiags := hclsyntax.ParseConfig([]byte(tc.cfg), "test.tf", hcl.InitialPos) if len(pDiags) > 0 { t.Fatal(pDiags) } - err := d.LoadFile("test.tf", f) - if err != nil { - t.Fatal(err) - } + + d := testPathDecoder(t, &PathContext{ + Schema: bodySchema, + ReferenceTargets: tc.refs, + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) tokens, err := d.SemanticTokensInFile("test.tf") if err != nil { diff --git a/decoder/semantic_tokens_test.go b/decoder/semantic_tokens_test.go index 0c363dbb..5238f50f 100644 --- a/decoder/semantic_tokens_test.go +++ b/decoder/semantic_tokens_test.go @@ -14,16 +14,16 @@ import ( ) func TestDecoder_SemanticTokensInFile_emptyBody(t *testing.T) { - d := NewDecoder() f := &hcl.File{ Body: hcl.EmptyBody(), } - err := d.LoadFile("test.tf", f) - if err != nil { - t.Fatal(err) - } + d := testPathDecoder(t, &PathContext{ + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) - _, err = d.SemanticTokensInFile("test.tf") + _, err := d.SemanticTokensInFile("test.tf") unknownFormatErr := &UnknownFileFormatError{} if !errors.As(err, &unknownFormatErr) { t.Fatal("expected UnknownFileFormatError for empty body") @@ -31,7 +31,6 @@ func TestDecoder_SemanticTokensInFile_emptyBody(t *testing.T) { } func TestDecoder_SemanticTokensInFile_json(t *testing.T) { - d := NewDecoder() f, pDiags := json.Parse([]byte(`{ "customblock": { "label1": {} @@ -40,12 +39,14 @@ func TestDecoder_SemanticTokensInFile_json(t *testing.T) { if len(pDiags) > 0 { t.Fatal(pDiags) } - err := d.LoadFile("test.tf.json", f) - if err != nil { - t.Fatal(err) - } - _, err = d.SemanticTokensInFile("test.tf.json") + d := testPathDecoder(t, &PathContext{ + Files: map[string]*hcl.File{ + "test.tf.json": f, + }, + }) + + _, err := d.SemanticTokensInFile("test.tf.json") unknownFormatErr := &UnknownFileFormatError{} if !errors.As(err, &unknownFormatErr) { t.Fatal("expected UnknownFileFormatError for JSON body") @@ -53,15 +54,16 @@ func TestDecoder_SemanticTokensInFile_json(t *testing.T) { } func TestDecoder_SemanticTokensInFile_zeroByteContent(t *testing.T) { - d := NewDecoder() f, pDiags := hclsyntax.ParseConfig([]byte{}, "test.tf", hcl.InitialPos) if len(pDiags) > 0 { t.Fatal(pDiags) } - err := d.LoadFile("test.tf", f) - if err != nil { - t.Fatal(err) - } + + d := testPathDecoder(t, &PathContext{ + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) tokens, err := d.SemanticTokensInFile("test.tf") if err != nil { @@ -74,17 +76,18 @@ func TestDecoder_SemanticTokensInFile_zeroByteContent(t *testing.T) { } func TestDecoder_SemanticTokensInFile_fileNotFound(t *testing.T) { - d := NewDecoder() f, pDiags := hclsyntax.ParseConfig([]byte{}, "test.tf", hcl.InitialPos) if len(pDiags) > 0 { t.Fatal(pDiags) } - err := d.LoadFile("test.tf", f) - if err != nil { - t.Fatal(err) - } - _, err = d.SemanticTokensInFile("foobar.tf") + d := testPathDecoder(t, &PathContext{ + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) + + _, err := d.SemanticTokensInFile("foobar.tf") notFoundErr := &FileNotFoundError{} if !errors.As(err, ¬FoundErr) { t.Fatal("expected FileNotFoundError for non-existent file") @@ -92,8 +95,7 @@ func TestDecoder_SemanticTokensInFile_fileNotFound(t *testing.T) { } func TestDecoder_SemanticTokensInFile_basic(t *testing.T) { - d := NewDecoder() - d.SetSchema(&schema.BodySchema{ + bodySchema := &schema.BodySchema{ Blocks: map[string]*schema.BlockSchema{ "module": { Body: &schema.BodySchema{ @@ -115,7 +117,7 @@ func TestDecoder_SemanticTokensInFile_basic(t *testing.T) { }, }, }, - }) + } testCfg := []byte(`module "ref" { source = "./sub" @@ -130,10 +132,13 @@ resource "vault_auth_backend" "blah" { if len(pDiags) > 0 { t.Fatal(pDiags) } - err := d.LoadFile("test.tf", f) - if err != nil { - t.Fatal(err) - } + + d := testPathDecoder(t, &PathContext{ + Schema: bodySchema, + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) tokens, err := d.SemanticTokensInFile("test.tf") if err != nil { @@ -290,8 +295,7 @@ resource "vault_auth_backend" "blah" { } func TestDecoder_SemanticTokensInFile_dependentSchema(t *testing.T) { - d := NewDecoder() - d.SetSchema(&schema.BodySchema{ + bodySchema := &schema.BodySchema{ Blocks: map[string]*schema.BlockSchema{ "resource": { Labels: []*schema.LabelSchema{ @@ -319,7 +323,7 @@ func TestDecoder_SemanticTokensInFile_dependentSchema(t *testing.T) { }, }, }, - }) + } testCfg := []byte(`resource "vault_auth_backend" "alpha" { default_lease_ttl_seconds = 1 @@ -334,10 +338,13 @@ resource "aws_instance" "beta" { if len(pDiags) > 0 { t.Fatal(pDiags) } - err := d.LoadFile("test.tf", f) - if err != nil { - t.Fatal(err) - } + + d := testPathDecoder(t, &PathContext{ + Schema: bodySchema, + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) tokens, err := d.SemanticTokensInFile("test.tf") if err != nil { @@ -532,8 +539,7 @@ resource "aws_instance" "beta" { } func TestDecoder_SemanticTokensInFile_typeDeclaration(t *testing.T) { - d := NewDecoder() - d.SetSchema(&schema.BodySchema{ + bodySchema := &schema.BodySchema{ Blocks: map[string]*schema.BlockSchema{ "variable": { Labels: []*schema.LabelSchema{ @@ -548,7 +554,7 @@ func TestDecoder_SemanticTokensInFile_typeDeclaration(t *testing.T) { }, }, }, - }) + } testCfg := []byte(`variable "meh" { type = string @@ -568,10 +574,13 @@ variable "bah" { if len(pDiags) > 0 { t.Fatal(pDiags) } - err := d.LoadFile("test.tf", f) - if err != nil { - t.Fatal(err) - } + + d := testPathDecoder(t, &PathContext{ + Schema: bodySchema, + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) tokens, err := d.SemanticTokensInFile("test.tf") if err != nil { diff --git a/decoder/symbol.go b/decoder/symbol.go index 7d26ccd5..d0190d22 100644 --- a/decoder/symbol.go +++ b/decoder/symbol.go @@ -12,6 +12,7 @@ type symbolImplSigil struct{} // Symbol represents any attribute, or block (and its nested blocks or attributes) type Symbol interface { + Path() string Name() string NestedSymbols() []Symbol Range() hcl.Range @@ -24,6 +25,7 @@ type BlockSymbol struct { Type string Labels []string + dirPath string rng hcl.Range nestedSymbols []Symbol } @@ -60,11 +62,16 @@ func (bs *BlockSymbol) Range() hcl.Range { return bs.rng } +func (bs *BlockSymbol) Path() string { + return bs.dirPath +} + // AttributeSymbol is Symbol implementation representing an attribute type AttributeSymbol struct { AttrName string ExprKind lang.SymbolExprKind + dirPath string rng hcl.Range nestedSymbols []Symbol } @@ -97,10 +104,15 @@ func (as *AttributeSymbol) Range() hcl.Range { return as.rng } +func (as *AttributeSymbol) Path() string { + return as.dirPath +} + type ExprSymbol struct { ExprName string ExprKind lang.SymbolExprKind + dirPath string rng hcl.Range nestedSymbols []Symbol } @@ -132,3 +144,7 @@ func (as *ExprSymbol) NestedSymbols() []Symbol { func (as *ExprSymbol) Range() hcl.Range { return as.rng } + +func (as *ExprSymbol) Path() string { + return as.dirPath +} diff --git a/decoder/symbols.go b/decoder/symbols.go index 719e1831..02b659ca 100644 --- a/decoder/symbols.go +++ b/decoder/symbols.go @@ -1,6 +1,7 @@ package decoder import ( + "context" "fmt" "sort" "strings" @@ -17,7 +18,7 @@ import ( // A symbol is typically represented by a block or an attribute. // // Symbols within JSON files require schema to be present for decoding. -func (d *Decoder) SymbolsInFile(filename string) ([]Symbol, error) { +func (d *PathDecoder) SymbolsInFile(filename string) ([]Symbol, error) { f, err := d.fileByName(filename) if err != nil { return nil, err @@ -28,36 +29,45 @@ func (d *Decoder) SymbolsInFile(filename string) ([]Symbol, error) { return nil, &UnknownFileFormatError{Filename: filename} } - return d.symbolsForBody(f.Body, d.rootSchema), nil + return d.symbolsForBody(f.Body, d.pathCtx.Schema), nil } -func (d *Decoder) symbolsInFile(filename string) ([]Symbol, error) { +func (d *PathDecoder) symbolsInFile(filename string) ([]Symbol, error) { f, err := d.fileByName(filename) if err != nil { return nil, err } - _, isHcl := f.Body.(*hclsyntax.Body) - if !isHcl { - d.rootSchemaMu.RLock() - defer d.rootSchemaMu.RUnlock() - } - - return d.symbolsForBody(f.Body, d.rootSchema), nil + return d.symbolsForBody(f.Body, d.pathCtx.Schema), nil } // Symbols returns a hierarchy of symbols matching the query -// in all loaded files (typically whole module). +// in all directories and loaded files within. // Query can be empty, as per LSP's workspace/symbol request, // in which case all symbols are returned. // // A symbol is typically represented by a block or an attribute. // // Symbols within JSON files require schema to be present for decoding. -func (d *Decoder) Symbols(query string) ([]Symbol, error) { +func (d *Decoder) Symbols(ctx context.Context, query string) ([]Symbol, error) { symbols := make([]Symbol, 0) - files := d.Filenames() + for _, path := range d.pathReader.Paths() { + pathDecoder := d.Path(ctx, path) + dirSymbols, err := pathDecoder.symbols(query) + if err != nil { + continue + } + + symbols = append(symbols, dirSymbols...) + } + + return symbols, nil +} + +func (d *PathDecoder) symbols(query string) ([]Symbol, error) { + symbols := make([]Symbol, 0) + files := d.filenames() for _, filename := range files { fSymbols, err := d.symbolsInFile(filename) @@ -75,7 +85,7 @@ func (d *Decoder) Symbols(query string) ([]Symbol, error) { return symbols, nil } -func (d *Decoder) symbolsForBody(body hcl.Body, bodySchema *schema.BodySchema) []Symbol { +func (d *PathDecoder) symbolsForBody(body hcl.Body, bodySchema *schema.BodySchema) []Symbol { symbols := make([]Symbol, 0) if body == nil { return symbols @@ -87,8 +97,9 @@ func (d *Decoder) symbolsForBody(body hcl.Body, bodySchema *schema.BodySchema) [ symbols = append(symbols, &AttributeSymbol{ AttrName: name, ExprKind: symbolExprKind(attr.Expr), + dirPath: d.pathCtx.DirPath, rng: attr.Range, - nestedSymbols: nestedSymbolsForExpr(attr.Expr), + nestedSymbols: d.nestedSymbolsForExpr(attr.Expr), }) } @@ -108,6 +119,7 @@ func (d *Decoder) symbolsForBody(body hcl.Body, bodySchema *schema.BodySchema) [ symbols = append(symbols, &BlockSymbol{ Type: block.Type, Labels: block.Labels, + dirPath: d.pathCtx.DirPath, rng: block.Range, nestedSymbols: d.symbolsForBody(block.Body, bSchema), }) @@ -145,7 +157,7 @@ func symbolExprKind(expr hcl.Expression) lang.SymbolExprKind { return nil } -func nestedSymbolsForExpr(expr hcl.Expression) []Symbol { +func (d *PathDecoder) nestedSymbolsForExpr(expr hcl.Expression) []Symbol { symbols := make([]Symbol, 0) switch e := expr.(type) { @@ -154,8 +166,9 @@ func nestedSymbolsForExpr(expr hcl.Expression) []Symbol { symbols = append(symbols, &ExprSymbol{ ExprName: fmt.Sprintf("%d", i), ExprKind: symbolExprKind(item), + dirPath: d.pathCtx.DirPath, rng: item.Range(), - nestedSymbols: nestedSymbolsForExpr(item), + nestedSymbols: d.nestedSymbolsForExpr(item), }) } case *hclsyntax.ObjectConsExpr: @@ -169,8 +182,9 @@ func nestedSymbolsForExpr(expr hcl.Expression) []Symbol { symbols = append(symbols, &ExprSymbol{ ExprName: key.AsString(), ExprKind: symbolExprKind(item.ValueExpr), + dirPath: d.pathCtx.DirPath, rng: hcl.RangeBetween(item.KeyExpr.Range(), item.ValueExpr.Range()), - nestedSymbols: nestedSymbolsForExpr(item.ValueExpr), + nestedSymbols: d.nestedSymbolsForExpr(item.ValueExpr), }) } } diff --git a/decoder/symbols_hcl_test.go b/decoder/symbols_hcl_test.go index 7e0cdf7d..07064073 100644 --- a/decoder/symbols_hcl_test.go +++ b/decoder/symbols_hcl_test.go @@ -1,6 +1,7 @@ package decoder import ( + "context" "testing" "github.com/google/go-cmp/cmp" @@ -11,15 +12,16 @@ import ( ) func TestDecoder_SymbolsInFile_hcl_zeroByteContent(t *testing.T) { - d := NewDecoder() f, pDiags := hclsyntax.ParseConfig([]byte{}, "test.tf", hcl.InitialPos) if len(pDiags) > 0 { t.Fatal(pDiags) } - err := d.LoadFile("test.tf", f) - if err != nil { - t.Fatal(err) - } + + d := testPathDecoder(t, &PathContext{ + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) symbols, err := d.SymbolsInFile("test.tf") if err != nil { @@ -32,8 +34,6 @@ func TestDecoder_SymbolsInFile_hcl_zeroByteContent(t *testing.T) { } func TestDecoder_Symbols_hcl_basic(t *testing.T) { - d := NewDecoder() - testCfg1 := []byte(` resource "aws_vpc" "main" { cidr_block = "10.0.0.0/16" @@ -54,16 +54,20 @@ provider "google" { t.Fatal(pDiags) } - err := d.LoadFile("first.tf", f1) - if err != nil { - t.Fatal(err) - } - err = d.LoadFile("second.tf", f2) - if err != nil { - t.Fatal(err) - } + dirPath := t.TempDir() + d := NewDecoder(&testPathReader{ + paths: map[string]*PathContext{ + dirPath: { + DirPath: dirPath, + Files: map[string]*hcl.File{ + "first.tf": f1, + "second.tf": f2, + }, + }, + }, + }) - symbols, err := d.Symbols("") + symbols, err := d.Symbols(context.Background(), "") if err != nil { t.Fatal(err) } @@ -75,6 +79,7 @@ provider "google" { "aws_vpc", "main", }, + dirPath: dirPath, rng: hcl.Range{ Filename: "first.tf", Start: hcl.Pos{Line: 2, Column: 1, Byte: 1}, @@ -84,6 +89,7 @@ provider "google" { &AttributeSymbol{ AttrName: "cidr_block", ExprKind: lang.LiteralTypeKind{Type: cty.String}, + dirPath: dirPath, rng: hcl.Range{ Filename: "first.tf", Start: hcl.Pos{Line: 3, Column: 3, Byte: 31}, @@ -98,6 +104,7 @@ provider "google" { Labels: []string{ "google", }, + dirPath: dirPath, rng: hcl.Range{ Filename: "second.tf", Start: hcl.Pos{Line: 2, Column: 1, Byte: 1}, @@ -107,6 +114,7 @@ provider "google" { &AttributeSymbol{ AttrName: "project", ExprKind: lang.LiteralTypeKind{Type: cty.String}, + dirPath: dirPath, rng: hcl.Range{ Filename: "second.tf", Start: hcl.Pos{Line: 3, Column: 3, Byte: 23}, @@ -117,6 +125,7 @@ provider "google" { &AttributeSymbol{ AttrName: "region", ExprKind: lang.LiteralTypeKind{Type: cty.String}, + dirPath: dirPath, rng: hcl.Range{ Filename: "second.tf", Start: hcl.Pos{Line: 4, Column: 3, Byte: 55}, @@ -135,8 +144,6 @@ provider "google" { } func TestDecoder_SymbolsInFile_hcl(t *testing.T) { - d := NewDecoder() - testCfg := []byte(` resource "aws_instance" "test" { subnet_ids = [ "one-1", "two-2" ] @@ -154,10 +161,17 @@ resource "aws_instance" "test" { t.Fatal(pDiags) } - err := d.LoadFile("test.tf", f) - if err != nil { - t.Fatal(err) - } + dirPath := t.TempDir() + d := NewDecoder(&testPathReader{ + paths: map[string]*PathContext{ + dirPath: { + DirPath: dirPath, + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }, + }, + }).Path(context.Background(), dirPath) symbols, err := d.SymbolsInFile("test.tf") if err != nil { @@ -171,6 +185,7 @@ resource "aws_instance" "test" { "aws_instance", "test", }, + dirPath: dirPath, rng: hcl.Range{ Filename: "test.tf", Start: hcl.Pos{Line: 2, Column: 1, Byte: 1}, @@ -180,6 +195,7 @@ resource "aws_instance" "test" { &AttributeSymbol{ AttrName: "subnet_ids", ExprKind: lang.TupleConsExprKind{}, + dirPath: dirPath, rng: hcl.Range{ Filename: "test.tf", Start: hcl.Pos{ @@ -199,6 +215,7 @@ resource "aws_instance" "test" { ExprKind: lang.LiteralTypeKind{ Type: cty.String, }, + dirPath: dirPath, rng: hcl.Range{ Filename: "test.tf", Start: hcl.Pos{ @@ -219,6 +236,7 @@ resource "aws_instance" "test" { ExprKind: lang.LiteralTypeKind{ Type: cty.String, }, + dirPath: dirPath, rng: hcl.Range{ Filename: "test.tf", Start: hcl.Pos{ @@ -239,6 +257,7 @@ resource "aws_instance" "test" { &AttributeSymbol{ AttrName: "configuration", ExprKind: lang.ObjectConsExprKind{}, + dirPath: dirPath, rng: hcl.Range{ Filename: "test.tf", Start: hcl.Pos{ @@ -258,6 +277,7 @@ resource "aws_instance" "test" { ExprKind: lang.LiteralTypeKind{ Type: cty.String, }, + dirPath: dirPath, rng: hcl.Range{ Filename: "test.tf", Start: hcl.Pos{ @@ -278,6 +298,7 @@ resource "aws_instance" "test" { ExprKind: lang.LiteralTypeKind{ Type: cty.Number, }, + dirPath: dirPath, rng: hcl.Range{ Filename: "test.tf", Start: hcl.Pos{ @@ -298,6 +319,7 @@ resource "aws_instance" "test" { ExprKind: lang.LiteralTypeKind{ Type: cty.Bool, }, + dirPath: dirPath, rng: hcl.Range{ Filename: "test.tf", Start: hcl.Pos{ @@ -318,6 +340,7 @@ resource "aws_instance" "test" { &AttributeSymbol{ AttrName: "random_kw", ExprKind: lang.TraversalExprKind{}, + dirPath: dirPath, rng: hcl.Range{ Filename: "test.tf", Start: hcl.Pos{ @@ -344,8 +367,6 @@ resource "aws_instance" "test" { } func TestDecoder_SymbolsInFile_hcl_unknownExpression(t *testing.T) { - d := NewDecoder() - testCfg := []byte(` resource "aws_instance" "test" { subnet_ids = [ var.test, "two-2" ] @@ -363,10 +384,17 @@ resource "aws_instance" "test" { t.Fatal(pDiags) } - err := d.LoadFile("test.tf", f) - if err != nil { - t.Fatal(err) - } + dirPath := t.TempDir() + d := NewDecoder(&testPathReader{ + paths: map[string]*PathContext{ + dirPath: { + DirPath: dirPath, + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }, + }, + }).Path(context.Background(), dirPath) symbols, err := d.SymbolsInFile("test.tf") if err != nil { @@ -380,6 +408,7 @@ resource "aws_instance" "test" { "aws_instance", "test", }, + dirPath: dirPath, rng: hcl.Range{ Filename: "test.tf", Start: hcl.Pos{Line: 2, Column: 1, Byte: 1}, @@ -389,6 +418,7 @@ resource "aws_instance" "test" { &AttributeSymbol{ AttrName: "subnet_ids", ExprKind: lang.TupleConsExprKind{}, + dirPath: dirPath, rng: hcl.Range{ Filename: "test.tf", Start: hcl.Pos{ @@ -406,6 +436,7 @@ resource "aws_instance" "test" { &ExprSymbol{ ExprName: "0", ExprKind: lang.TraversalExprKind{}, + dirPath: dirPath, rng: hcl.Range{ Filename: "test.tf", Start: hcl.Pos{ @@ -426,6 +457,7 @@ resource "aws_instance" "test" { ExprKind: lang.LiteralTypeKind{ Type: cty.String, }, + dirPath: dirPath, rng: hcl.Range{ Filename: "test.tf", Start: hcl.Pos{ @@ -446,6 +478,7 @@ resource "aws_instance" "test" { &AttributeSymbol{ AttrName: "configuration", ExprKind: lang.ObjectConsExprKind{}, + dirPath: dirPath, rng: hcl.Range{ Filename: "test.tf", Start: hcl.Pos{ @@ -463,6 +496,7 @@ resource "aws_instance" "test" { &ExprSymbol{ ExprName: "num", ExprKind: lang.TraversalExprKind{}, + dirPath: dirPath, rng: hcl.Range{ Filename: "test.tf", Start: hcl.Pos{ @@ -483,6 +517,7 @@ resource "aws_instance" "test" { &AttributeSymbol{ AttrName: "random_kw", ExprKind: lang.TraversalExprKind{}, + dirPath: dirPath, rng: hcl.Range{ Filename: "test.tf", Start: hcl.Pos{ @@ -509,8 +544,6 @@ resource "aws_instance" "test" { } func TestDecoder_Symbols_hcl_query(t *testing.T) { - d := NewDecoder() - testCfg1 := []byte(` resource "aws_vpc" "main" { cidr_block = "10.0.0.0/16" @@ -531,16 +564,20 @@ provider "google" { t.Fatal(pDiags) } - err := d.LoadFile("first.tf", f1) - if err != nil { - t.Fatal(err) - } - err = d.LoadFile("second.tf", f2) - if err != nil { - t.Fatal(err) - } + dirPath := t.TempDir() + d := NewDecoder(&testPathReader{ + paths: map[string]*PathContext{ + dirPath: { + DirPath: dirPath, + Files: map[string]*hcl.File{ + "first.tf": f1, + "second.tf": f2, + }, + }, + }, + }) - symbols, err := d.Symbols("google") + symbols, err := d.Symbols(context.Background(), "google") if err != nil { t.Fatal(err) } @@ -551,6 +588,7 @@ provider "google" { Labels: []string{ "google", }, + dirPath: dirPath, rng: hcl.Range{ Filename: "second.tf", Start: hcl.Pos{Line: 2, Column: 1, Byte: 1}, @@ -560,6 +598,7 @@ provider "google" { &AttributeSymbol{ AttrName: "project", ExprKind: lang.LiteralTypeKind{Type: cty.String}, + dirPath: dirPath, rng: hcl.Range{ Filename: "second.tf", Start: hcl.Pos{Line: 3, Column: 3, Byte: 23}, @@ -570,6 +609,7 @@ provider "google" { &AttributeSymbol{ AttrName: "region", ExprKind: lang.LiteralTypeKind{Type: cty.String}, + dirPath: dirPath, rng: hcl.Range{ Filename: "second.tf", Start: hcl.Pos{Line: 4, Column: 3, Byte: 55}, diff --git a/decoder/symbols_json_test.go b/decoder/symbols_json_test.go index b3087703..fda2e91d 100644 --- a/decoder/symbols_json_test.go +++ b/decoder/symbols_json_test.go @@ -1,6 +1,7 @@ package decoder import ( + "context" "testing" "github.com/google/go-cmp/cmp" @@ -10,20 +11,25 @@ import ( "github.com/zclconf/go-cty/cty" ) -func TestDecoder_SymbolsInFile_json(t *testing.T) { - d := NewDecoder() - +func TestDecoder_Symbols_json(t *testing.T) { f, diags := json.Parse([]byte(``), "test.tf.json") if len(diags) == 0 { t.Fatal("expected empty JSON file to fail parsing") } - err := d.LoadFile("test.tf.json", f) - if err != nil { - t.Fatal(err) - } + dirPath := t.TempDir() + d := NewDecoder(&testPathReader{ + paths: map[string]*PathContext{ + dirPath: { + DirPath: dirPath, + Files: map[string]*hcl.File{ + "test.tf.json": f, + }, + }, + }, + }) - symbols, err := d.Symbols("test.tf.json") + symbols, err := d.Symbols(context.Background(), "") if err != nil { t.Fatal(err) } @@ -33,19 +39,24 @@ func TestDecoder_SymbolsInFile_json(t *testing.T) { } func TestDecoder_Symbols_json_emptyFile(t *testing.T) { - d := NewDecoder() - f, diags := json.Parse([]byte(``), "test.tf.json") if len(diags) == 0 { t.Fatal("expected empty JSON file to fail parsing") } - err := d.LoadFile("test.tf.json", f) - if err != nil { - t.Fatal(err) - } + dirPath := t.TempDir() + d := NewDecoder(&testPathReader{ + paths: map[string]*PathContext{ + dirPath: { + DirPath: dirPath, + Files: map[string]*hcl.File{ + "test.tf.json": f, + }, + }, + }, + }) - symbols, err := d.Symbols("test.tf.json") + symbols, err := d.Symbols(context.Background(), "test.tf.json") if err != nil { t.Fatal(err) } @@ -55,18 +66,24 @@ func TestDecoder_Symbols_json_emptyFile(t *testing.T) { } func TestDecoder_Symbols_json_emptyBody(t *testing.T) { - d := NewDecoder() f, diags := json.Parse([]byte(`{}`), "test.tf.json") if len(diags) > 0 { t.Fatal(diags) } - err := d.LoadFile("test.tf.json", f) - if err != nil { - t.Fatal(err) - } + dirPath := t.TempDir() + d := NewDecoder(&testPathReader{ + paths: map[string]*PathContext{ + dirPath: { + DirPath: dirPath, + Files: map[string]*hcl.File{ + "test.tf.json": f, + }, + }, + }, + }) - symbols, err := d.Symbols("test.tf.json") + symbols, err := d.Symbols(context.Background(), "test.tf.json") if err != nil { t.Fatal(err) } @@ -76,8 +93,7 @@ func TestDecoder_Symbols_json_emptyBody(t *testing.T) { } func TestDecoder_Symbols_json_basic(t *testing.T) { - d := NewDecoder() - testSchema := &schema.BodySchema{ + bodySchema := &schema.BodySchema{ Blocks: map[string]*schema.BlockSchema{ "resource": { Labels: []*schema.LabelSchema{ @@ -103,7 +119,6 @@ func TestDecoder_Symbols_json_basic(t *testing.T) { }, }, } - d.SetSchema(testSchema) testCfg1 := []byte(`{ "resource": { @@ -132,16 +147,21 @@ func TestDecoder_Symbols_json_basic(t *testing.T) { t.Fatal(pDiags) } - err := d.LoadFile("first.tf.json", f1) - if err != nil { - t.Fatal(err) - } - err = d.LoadFile("second.tf.json", f2) - if err != nil { - t.Fatal(err) - } + dirPath := t.TempDir() + d := NewDecoder(&testPathReader{ + paths: map[string]*PathContext{ + dirPath: { + DirPath: dirPath, + Schema: bodySchema, + Files: map[string]*hcl.File{ + "first.tf.json": f1, + "second.tf.json": f2, + }, + }, + }, + }) - symbols, err := d.Symbols("") + symbols, err := d.Symbols(context.Background(), "") if err != nil { t.Fatal(err) } @@ -153,6 +173,7 @@ func TestDecoder_Symbols_json_basic(t *testing.T) { "aws_vpc", "main", }, + dirPath: dirPath, rng: hcl.Range{ Filename: "first.tf.json", Start: hcl.Pos{Line: 4, Column: 15, Byte: 49}, @@ -161,6 +182,7 @@ func TestDecoder_Symbols_json_basic(t *testing.T) { nestedSymbols: []Symbol{ &AttributeSymbol{ AttrName: "cidr_block", + dirPath: dirPath, rng: hcl.Range{ Filename: "first.tf.json", Start: hcl.Pos{Line: 5, Column: 9, Byte: 59}, @@ -175,6 +197,7 @@ func TestDecoder_Symbols_json_basic(t *testing.T) { Labels: []string{ "google", }, + dirPath: dirPath, rng: hcl.Range{ Filename: "second.tf.json", Start: hcl.Pos{Line: 3, Column: 15, Byte: 32}, @@ -183,6 +206,7 @@ func TestDecoder_Symbols_json_basic(t *testing.T) { nestedSymbols: []Symbol{ &AttributeSymbol{ AttrName: "project", + dirPath: dirPath, rng: hcl.Range{ Filename: "second.tf.json", Start: hcl.Pos{Line: 4, Column: 7, Byte: 40}, @@ -192,6 +216,7 @@ func TestDecoder_Symbols_json_basic(t *testing.T) { }, &AttributeSymbol{ AttrName: "region", + dirPath: dirPath, rng: hcl.Range{ Filename: "second.tf.json", Start: hcl.Pos{Line: 5, Column: 7, Byte: 74}, @@ -210,8 +235,7 @@ func TestDecoder_Symbols_json_basic(t *testing.T) { } func TestDecoder_Symbols_json_dependentBody(t *testing.T) { - d := NewDecoder() - testSchema := &schema.BodySchema{ + bodySchema := &schema.BodySchema{ Blocks: map[string]*schema.BlockSchema{ "resource": { Labels: []*schema.LabelSchema{ @@ -261,7 +285,6 @@ func TestDecoder_Symbols_json_dependentBody(t *testing.T) { }, }, } - d.SetSchema(testSchema) testCfg := []byte(`{ "resource": { @@ -283,20 +306,29 @@ func TestDecoder_Symbols_json_dependentBody(t *testing.T) { t.Fatal(pDiags) } - err := d.LoadFile("test.tf.json", f) - if err != nil { - t.Fatal(err) - } + dirPath := t.TempDir() + d := NewDecoder(&testPathReader{ + paths: map[string]*PathContext{ + dirPath: { + DirPath: dirPath, + Schema: bodySchema, + Files: map[string]*hcl.File{ + "test.tf.json": f, + }, + }, + }, + }) - symbols, err := d.Symbols("") + symbols, err := d.Symbols(context.Background(), "") if err != nil { t.Fatal(err) } expectedSymbols := []Symbol{ &BlockSymbol{ - Type: "resource", - Labels: []string{"aws_instance", "test"}, + Type: "resource", + Labels: []string{"aws_instance", "test"}, + dirPath: dirPath, rng: hcl.Range{ Filename: "test.tf.json", Start: hcl.Pos{Line: 4, Column: 15, Byte: 54}, @@ -305,6 +337,7 @@ func TestDecoder_Symbols_json_dependentBody(t *testing.T) { nestedSymbols: []Symbol{ &AttributeSymbol{ AttrName: "subnet_ids", + dirPath: dirPath, rng: hcl.Range{ Filename: "test.tf.json", Start: hcl.Pos{Line: 5, Column: 9, Byte: 64}, @@ -313,8 +346,9 @@ func TestDecoder_Symbols_json_dependentBody(t *testing.T) { nestedSymbols: []Symbol{}, }, &BlockSymbol{ - Type: "configuration", - Labels: []string{}, + Type: "configuration", + Labels: []string{}, + dirPath: dirPath, rng: hcl.Range{ Filename: "test.tf.json", Start: hcl.Pos{Line: 6, Column: 26, Byte: 125}, @@ -323,6 +357,7 @@ func TestDecoder_Symbols_json_dependentBody(t *testing.T) { nestedSymbols: []Symbol{ &AttributeSymbol{ AttrName: "name", + dirPath: dirPath, rng: hcl.Range{ Filename: "test.tf.json", Start: hcl.Pos{Line: 7, Column: 11, Byte: 137}, @@ -332,6 +367,7 @@ func TestDecoder_Symbols_json_dependentBody(t *testing.T) { }, &AttributeSymbol{ AttrName: "num", + dirPath: dirPath, rng: hcl.Range{ Filename: "test.tf.json", Start: hcl.Pos{Line: 8, Column: 11, Byte: 163}, @@ -341,6 +377,7 @@ func TestDecoder_Symbols_json_dependentBody(t *testing.T) { }, &AttributeSymbol{ AttrName: "boolattr", + dirPath: dirPath, rng: hcl.Range{ Filename: "test.tf.json", Start: hcl.Pos{Line: 9, Column: 11, Byte: 184}, @@ -352,6 +389,7 @@ func TestDecoder_Symbols_json_dependentBody(t *testing.T) { }, &AttributeSymbol{ AttrName: "random_kw", + dirPath: dirPath, rng: hcl.Range{ Filename: "test.tf.json", Start: hcl.Pos{Line: 11, Column: 9, Byte: 220}, @@ -370,8 +408,7 @@ func TestDecoder_Symbols_json_dependentBody(t *testing.T) { } func TestDecoder_Symbols_json_unknownExpression(t *testing.T) { - d := NewDecoder() - testSchema := &schema.BodySchema{ + bodySchema := &schema.BodySchema{ Blocks: map[string]*schema.BlockSchema{ "resource": { Labels: []*schema.LabelSchema{ @@ -411,7 +448,6 @@ func TestDecoder_Symbols_json_unknownExpression(t *testing.T) { }, }, } - d.SetSchema(testSchema) testCfg := []byte(`{ "resource": { @@ -434,20 +470,29 @@ func TestDecoder_Symbols_json_unknownExpression(t *testing.T) { t.Fatal(pDiags) } - err := d.LoadFile("test.tf.json", f) - if err != nil { - t.Fatal(err) - } + dirPath := t.TempDir() + d := NewDecoder(&testPathReader{ + paths: map[string]*PathContext{ + dirPath: { + DirPath: dirPath, + Schema: bodySchema, + Files: map[string]*hcl.File{ + "first.tf.json": f, + }, + }, + }, + }) - symbols, err := d.Symbols("") + symbols, err := d.Symbols(context.Background(), "") if err != nil { t.Fatal(err) } expectedSymbols := []Symbol{ &BlockSymbol{ - Type: "resource", - Labels: []string{"aws_instance", "test"}, + Type: "resource", + Labels: []string{"aws_instance", "test"}, + dirPath: dirPath, rng: hcl.Range{ Filename: "test.tf.json", Start: hcl.Pos{Line: 4, Column: 15, Byte: 54}, @@ -456,6 +501,7 @@ func TestDecoder_Symbols_json_unknownExpression(t *testing.T) { nestedSymbols: []Symbol{ &AttributeSymbol{ AttrName: "subnet_ids", + dirPath: dirPath, rng: hcl.Range{ Filename: "test.tf.json", Start: hcl.Pos{Line: 5, Column: 9, Byte: 64}, @@ -464,8 +510,9 @@ func TestDecoder_Symbols_json_unknownExpression(t *testing.T) { nestedSymbols: []Symbol{}, }, &BlockSymbol{ - Type: "configuration", - Labels: []string{}, + Type: "configuration", + Labels: []string{}, + dirPath: dirPath, rng: hcl.Range{ Filename: "test.tf.json", Start: hcl.Pos{Line: 6, Column: 26, Byte: 131}, @@ -474,6 +521,7 @@ func TestDecoder_Symbols_json_unknownExpression(t *testing.T) { nestedSymbols: []Symbol{ &AttributeSymbol{ AttrName: "num", + dirPath: dirPath, rng: hcl.Range{ Filename: "test.tf.json", Start: hcl.Pos{Line: 8, Column: 11, Byte: 175}, @@ -485,6 +533,7 @@ func TestDecoder_Symbols_json_unknownExpression(t *testing.T) { }, &AttributeSymbol{ AttrName: "random_kw", + dirPath: dirPath, rng: hcl.Range{ Filename: "test.tf.json", Start: hcl.Pos{Line: 12, Column: 9, Byte: 295}, diff --git a/decoder/symbols_test.go b/decoder/symbols_test.go index 88f9dbc8..de303db7 100644 --- a/decoder/symbols_test.go +++ b/decoder/symbols_test.go @@ -9,16 +9,17 @@ import ( ) func TestDecoder_SymbolsInFile_emptyBody(t *testing.T) { - d := NewDecoder() f := &hcl.File{ Body: hcl.EmptyBody(), } - err := d.LoadFile("test.tf", f) - if err != nil { - t.Fatal(err) - } - _, err = d.SymbolsInFile("test.tf") + d := testPathDecoder(t, &PathContext{ + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) + + _, err := d.SymbolsInFile("test.tf") unknownFormatErr := &UnknownFileFormatError{} if !errors.As(err, &unknownFormatErr) { t.Fatal("expected UnknownFileFormatError for empty body") @@ -26,17 +27,18 @@ func TestDecoder_SymbolsInFile_emptyBody(t *testing.T) { } func TestDecoder_SymbolsInFile_fileNotFound(t *testing.T) { - d := NewDecoder() f, pDiags := hclsyntax.ParseConfig([]byte{}, "test.tf", hcl.InitialPos) if len(pDiags) > 0 { t.Fatal(pDiags) } - err := d.LoadFile("test.tf", f) - if err != nil { - t.Fatal(err) - } - _, err = d.SymbolsInFile("foobar.tf") + d := testPathDecoder(t, &PathContext{ + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) + + _, err := d.SymbolsInFile("foobar.tf") notFoundErr := &FileNotFoundError{} if !errors.As(err, ¬FoundErr) { t.Fatal("expected FileNotFoundError for non-existent file")