Skip to content

Commit

Permalink
feat(variables): add variables resolving to templatecontext
Browse files Browse the repository at this point in the history
  • Loading branch information
qvalentin committed Jul 1, 2024
1 parent 75f7fb9 commit 713c2c6
Show file tree
Hide file tree
Showing 18 changed files with 240 additions and 40 deletions.
15 changes: 8 additions & 7 deletions internal/handler/completion_main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"context"
"errors"
"os"
"strings"
"testing"

"github.com/mrjosh/helm-ls/internal/adapter/yamlls"
Expand All @@ -13,7 +12,6 @@ import (
lsplocal "github.com/mrjosh/helm-ls/internal/lsp"
"github.com/mrjosh/helm-ls/internal/util"
"github.com/stretchr/testify/assert"
"go.lsp.dev/protocol"
lsp "go.lsp.dev/protocol"
"go.lsp.dev/uri"
)
Expand Down Expand Up @@ -176,17 +174,20 @@ func TestCompletionMainSingleLines(t *testing.T) {
{`Test completion on {{ range .Values.ingress.hosts }} {{ .^ }} {{ end }}`, []string{"host", "paths"}, []string{}, nil},
{`Test completion on {{ range .Values.ingress.hosts }} {{ .ho^ }} {{ end }}`, []string{"host", "paths"}, []string{}, nil},
{`Test completion on {{ range .Values.ingress.hosts }} {{ range .paths }} {{ .^ }} {{ end }} {{ end }}`, []string{"pathType", "path"}, []string{}, nil},
{`Test completion on {{ root := . }} {{ $root.test.^ }}`, []string{}, []string{}, errors.New("[$root test ] is no valid template context for helm")},
{`Test completion on {{ $root := . }} {{ $root.test.^ }}`, []string{}, []string{}, errors.New("[test ] is no valid template context for helm")},
{`Test completion on {{ range $type, $config := $.Values.deployments }} {{ .^ }} {{ end }}`, []string{"some"}, []string{}, nil},
{`Test completion on {{ range $type, $config := $.Values.deployments }} {{ .s^ }} {{ end }}`, []string{"some"}, []string{}, nil},
{`Test completion on {{ range $type, $config := $.Values.deployments }} {{ $config.^ }} {{ end }}`, []string{"some"}, []string{}, nil},
{`Test completion on {{ range .Values.deploymentsWithNestedStuff }} {{ .hpa.cpuUtilization.^ }} {{ end }}`, []string{"targetAverageUtilization", "enabled"}, []string{}, nil},
{`Test completion on {{ range $type, $config := .Values.deploymentsWithNestedStuff }} {{ .hpa.cpuUtilization.^ }} {{ end }}`, []string{"targetAverageUtilization", "enabled"}, []string{}, nil},
{`Test completion on {{ range $type, $config := .Values.deploymentsWithNestedStuff }} {{ $config.hpa.cpuUtilization.^ }} {{ end }}`, []string{"targetAverageUtilization", "enabled"}, []string{}, nil},
{`Test completion on {{ range $type, $config := $.Values.deployments }} {{ $config.s^ }} {{ end }}`, []string{"some"}, []string{}, nil},
}

for _, tt := range testCases {
t.Run(tt.templateWithMark, func(t *testing.T) {
// seen chars up to ^
col := strings.Index(tt.templateWithMark, "^")
buf := strings.Replace(tt.templateWithMark, "^", "", 1)
pos := protocol.Position{Line: 0, Character: uint32(col)}
pos, buf := getPositionForMarkedTestLine(tt.templateWithMark)

// to get the correct values file ../../testdata/example/values.yaml
fileURI := uri.File("../../testdata/example/templates/completion-test.yaml")

Expand Down
15 changes: 15 additions & 0 deletions internal/handler/helpers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package handler

import (
"strings"

"go.lsp.dev/protocol"
)

// Takes a string with a mark (^) in it and returns the position and the string without the mark
func getPositionForMarkedTestLine(buf string) (protocol.Position, string) {
col := strings.Index(buf, "^")
buf = strings.Replace(buf, "^", "", 1)
pos := protocol.Position{Line: 0, Character: uint32(col)}
return pos, buf
}
6 changes: 3 additions & 3 deletions internal/handler/hover_main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,16 @@ func TestHoverMain(t *testing.T) {
Line: 74,
Character: 50,
},
expected: "$root.Values.deployments",
expected: fmt.Sprintf("### %s\n%s\n\n\n", filepath.Join("..", "..", "testdata", "example", "values.yaml"), "first:\n some: value\nsecond:\n some: value"),
expectedError: nil,
},
{
desc: "Test hover on template context with variables in range loop",
position: lsp.Position{
Line: 80,
Character: 35,
Character: 31,
},
expected: "$config.hpa.minReplicas",
expected: fmt.Sprintf("### %s\n%s\n\n", filepath.Join("..", "..", "testdata", "example", "values.yaml"), "value"),
expectedError: nil,
},
{
Expand Down
9 changes: 0 additions & 9 deletions internal/handler/references_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package handler
import (
"context"
"os"
"strings"
"testing"

"github.com/mrjosh/helm-ls/internal/adapter/yamlls"
Expand Down Expand Up @@ -241,11 +240,3 @@ func TestRefercesSingleLines(t *testing.T) {
})
}
}

// Takes a string with a mark (^) in it and returns the position and the string without the mark
func getPositionForMarkedTestLine(buf string) (protocol.Position, string) {
col := strings.Index(buf, "^")
buf = strings.Replace(buf, "^", "", 1)
pos := protocol.Position{Line: 0, Character: uint32(col)}
return pos, buf
}
16 changes: 14 additions & 2 deletions internal/language_features/built_in_objects.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,18 +32,24 @@ func (f *BuiltInObjectsFeature) AppropriateForNode() bool {
if err != nil || len(templateContext) != 1 {
return false
}

for _, allowedBuiltIn := range allowedBuiltIns {
if templateContext[0] == allowedBuiltIn {
return true
}
}

return false
}

func (f *BuiltInObjectsFeature) References() (result []lsp.Location, err error) {
templateContext, _ := f.getTemplateContext()
templateContext, err := f.getTemplateContext()
if err != nil {
return []lsp.Location{}, err
}

locations := f.getReferencesFromSymbolTable(templateContext)

return append(locations, f.getDefinitionLocations(templateContext)...), err
}

Expand All @@ -55,6 +61,7 @@ func (f *BuiltInObjectsFeature) getDefinitionLocations(templateContext lsplocal.
for _, valueFile := range f.Chart.ValuesFiles.AllValuesFiles() {
locations = append(locations, lsp.Location{URI: valueFile.URI})
}

return locations
case "Chart":

Check failure on line 66 in internal/language_features/built_in_objects.go

View workflow job for this annotation

GitHub Actions / lint (1.21.5, ubuntu-latest)

string `Chart` has 3 occurrences, make it a constant (goconst)
return []lsp.Location{{URI: f.Chart.ChartMetadata.URI}}
Expand All @@ -64,9 +71,13 @@ func (f *BuiltInObjectsFeature) getDefinitionLocations(templateContext lsplocal.
}

func (f *BuiltInObjectsFeature) Hover() (string, error) {
templateContext, _ := f.getTemplateContext()
templateContext, err := f.getTemplateContext()
if err != nil {
return "", err
}

docs, err := f.builtInOjectDocsLookup(templateContext[0], helmdocs.BuiltInObjects)

return docs.Doc, err
}

Expand All @@ -75,5 +86,6 @@ func (f *BuiltInObjectsFeature) Definition() (result []lsp.Location, err error)
if err != nil {
return []lsp.Location{}, err
}

return f.getDefinitionLocations(templateContext), nil
}
2 changes: 2 additions & 0 deletions internal/language_features/generic_template_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ func (f *GenericTemplateContextFeature) getTemplateContext() (lsplocal.TemplateC

func (f *GenericTemplateContextFeature) getReferencesFromSymbolTable(templateContext lsplocal.TemplateContext) []lsp.Location {
locations := []lsp.Location{}

for _, doc := range f.GenericDocumentUseCase.DocumentStore.GetAllDocs() {
referenceRanges := doc.SymbolTable.GetTemplateContextRanges(templateContext)
for _, referenceRange := range referenceRanges {
Expand All @@ -35,5 +36,6 @@ func (f *GenericTemplateContextFeature) builtInOjectDocsLookup(key string, docs
return item, nil
}
}

return helmdocs.HelmDocumentation{}, fmt.Errorf("key %s not found on built-in object", key)
}
31 changes: 30 additions & 1 deletion internal/lsp/symbol_table.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ func (t TemplateContext) Copy() TemplateContext {
return append(TemplateContext{}, t...)
}

// Return everything except the first context
func (t TemplateContext) Tail() TemplateContext {
return t[1:]
}
Expand All @@ -25,11 +26,38 @@ func (t TemplateContext) IsVariable() bool {
return len(t) > 0 && strings.HasPrefix(t[0], "$")
}

// Adds a suffix to the last context
func (t TemplateContext) AppendSuffix(suffix string) TemplateContext {
t[len(t)-1] = t[len(t)-1] + suffix
return t
}

// Adds a new context to the beginning
func (t TemplateContext) PrependContext(context string) TemplateContext {
if context == "." {
return t
}
return append(TemplateContext{ensureNoLeadingDot(context)}, t...)
}

func NewTemplateContext(string string) TemplateContext {
if string == "." {
return TemplateContext{}
}
splitted := strings.Split(string, ".")
if len(splitted) > 0 && splitted[0] == "" {
return splitted[1:]
}
return splitted
}

func ensureNoLeadingDot(context string) string {
if context[0] == '.' && len(context) > 1 {
return context[1:]
}
return context
}

type SymbolTable struct {
contexts map[string][]sitter.Range
contextsReversed map[sitter.Range]TemplateContext
Expand Down Expand Up @@ -58,6 +86,7 @@ func (s *SymbolTable) AddTemplateContext(templateContext TemplateContext, pointR
// we can just remove it from the template context
templateContext = templateContext.Tail()
}

s.contexts[templateContext.Format()] = append(s.contexts[templateContext.Format()], pointRange)
sliceCopy := make(TemplateContext, len(templateContext))
copy(sliceCopy, templateContext)
Expand All @@ -74,7 +103,7 @@ func (s *SymbolTable) GetTemplateContext(pointRange sitter.Range) (TemplateConte
return result, fmt.Errorf("no template context found")
}
// return a copy to never modify the original
return result.Copy(), nil
return s.ResolveVariablesInTemplateContext(result.Copy(), pointRange)
}

func (s *SymbolTable) AddIncludeDefinition(symbol string, pointRange sitter.Range) {
Expand Down
12 changes: 8 additions & 4 deletions internal/lsp/symbol_table_template_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ func (v *TemplateContextVisitor) PushContextMany(context []string) {
}

func (v *TemplateContextVisitor) PopContext() {
if len(v.currentContext) == 0 {
return
}
v.currentContext = v.currentContext[:len(v.currentContext)-1]
}

Expand Down Expand Up @@ -60,10 +63,11 @@ func (v *TemplateContextVisitor) Enter(node *sitter.Node) {
v.symbolTable.AddTemplateContext(append(v.currentContext, content), GetRangeForNode(node.ChildByFieldName("name")))
case gotemplate.NodeTypeUnfinishedSelectorExpression:
operandNode := node.ChildByFieldName("operand")
if operandNode.Type() == gotemplate.NodeTypeVariable {
v.StashContext()
content := getContextForSelectorExpression(operandNode, v.content)
if !content.IsVariable() {
content = append(v.currentContext, content...)
}
v.symbolTable.AddTemplateContext(append(getContextForSelectorExpression(operandNode, v.content), ""),
v.symbolTable.AddTemplateContext(append(content, ""),
GetRangeForNode(node.Child(int(node.ChildCount())-1)))
case gotemplate.NodeTypeSelectorExpression:
operandNode := node.ChildByFieldName("operand")
Expand All @@ -76,7 +80,7 @@ func (v *TemplateContextVisitor) Enter(node *sitter.Node) {

func (v *TemplateContextVisitor) Exit(node *sitter.Node) {
switch node.Type() {
case gotemplate.NodeTypeSelectorExpression, gotemplate.NodeTypeUnfinishedSelectorExpression:
case gotemplate.NodeTypeSelectorExpression:
operandNode := node.ChildByFieldName("operand")
if operandNode.Type() == gotemplate.NodeTypeVariable {
v.PopContext()
Expand Down
41 changes: 41 additions & 0 deletions internal/lsp/symbol_table_template_context_variables.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package lsp

import (
"fmt"

sitter "github.com/smacker/go-tree-sitter"
)

func (s *SymbolTable) ResolveVariablesInTemplateContext(templateContext TemplateContext, pointRange sitter.Range) (TemplateContext, error) {
if !templateContext.IsVariable() {
return templateContext, nil
}

variableName := templateContext[0]
if variableName == "$" {
return templateContext.Tail(), nil
}

variableDefinitions := s.variableDefinitions[variableName]

if len(variableDefinitions) == 0 {
return templateContext, fmt.Errorf("variable %s not found", variableName)
}

definition, err := findDefinitionForRange(variableDefinitions, pointRange)
if err != nil {
return templateContext, fmt.Errorf("variable %s not found %e", variableName, err)
}

prefix := getPrefixTemplateContextForVariable(definition)

return s.ResolveVariablesInTemplateContext(append(prefix, templateContext.Tail()...), pointRange)
}

func getPrefixTemplateContextForVariable(definition VariableDefinition) TemplateContext {
prefix := NewTemplateContext(definition.Value)
if definition.VariableType == VariableTypeRangeValue && len(prefix) > 0 {
prefix[len(prefix)-1] = prefix[len(prefix)-1] + "[]"
}
return prefix
}
42 changes: 42 additions & 0 deletions internal/lsp/symbol_table_template_context_variables_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package lsp

import (
"strings"
"testing"

sitter "github.com/smacker/go-tree-sitter"
"github.com/stretchr/testify/assert"
)

func TestResolveVariablesInTemplateContext(t *testing.T) {
tests := []struct {
template string
templateCtx TemplateContext
expectedCtx TemplateContext
expectedErr error
}{
{"{{ $values := .Values }} {{ $values.te^st }}", TemplateContext{"$values", "test"}, TemplateContext{"Values", "test"}, nil},
{"{{- range $type, $config := .Values.deployments }} {{ $config.te^st }}", TemplateContext{"$config", "test"}, TemplateContext{"Values", "deployments[]", "test"}, nil},
{" {{ $values := .Values }} {{- range $type, $config := $values.deployments }} {{ $config.te^st }}", TemplateContext{"$config", "test"}, TemplateContext{"Values", "deployments[]", "test"}, nil},
}

for _, tt := range tests {
t.Run(tt.template, func(t *testing.T) {
col := strings.Index(tt.template, "^")
buf := strings.Replace(tt.template, "^", "", 1)
ast := ParseAst(nil, tt.template)
symbolTable := NewSymbolTable(ast, []byte(buf))

result, err := symbolTable.ResolveVariablesInTemplateContext(
tt.templateCtx, sitter.Range{StartByte: uint32(col), EndByte: uint32(col + 1)})

if tt.expectedErr != nil {
assert.Error(t, err)
assert.Equal(t, tt.expectedErr, err)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.expectedCtx, result)
}
})
}
}
29 changes: 28 additions & 1 deletion internal/lsp/symbol_table_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,33 @@ func TestSymbolTableForValuesSingleTests(t *testing.T) {
},
foundContextsLen: 3,
},
{
template: `{{- range $type, $config := .Values.deployments }} {{ .test.nested }} {{ end }} `,
path: []string{"Values", "deployments[]", "test", "nested"},
startPoint: sitter.Point{
Row: 0,
Column: 60,
},
foundContextsLen: 4,
},
{
template: `{{- range $type, $config := .Values.deployments }} {{ .test.nested. }} {{ end }} `,
path: []string{"Values", "deployments[]", "test", "nested", ""},
startPoint: sitter.Point{
Row: 0,
Column: 66,
},
foundContextsLen: 5,
},
{
template: `{{- range $type, $config := .Values.deployments }} {{ $config.test.nested. }} {{ end }} `,
path: []string{"$config", "test", "nested", ""},
startPoint: sitter.Point{
Row: 0,
Column: 73,
},
foundContextsLen: 5,
},
}

for _, v := range testCases {
Expand All @@ -304,7 +331,7 @@ func TestSymbolTableForValuesSingleTests(t *testing.T) {
for _, v := range values {
points = append(points, v.StartPoint)
}
assert.Contains(t, points, v.startPoint)
assert.Contains(t, points, v.startPoint, "Ast was %s", ast.RootNode())
assert.Len(t, symbolTable.contexts, v.foundContextsLen)
})
}
Expand Down
Loading

0 comments on commit 713c2c6

Please sign in to comment.