package cmd
import (
"fmt"
"io"
"log"
"os"
"reflect"
"strings"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/spf13/cobra"
)
func typeString(node any, depth int) string {
leader := strings.Repeat(" ", depth)
t := strings.TrimPrefix(reflect.TypeOf(node).String(), "*")
t = strings.TrimPrefix(t, "hclsyntax.")
return leader + t
}
type counter struct {
depth int
maxLen int
}
func (c *counter) setMax(t string) {
if len(t) > c.maxLen {
c.maxLen = len(t)
}
}
func (c *counter) Enter(node hclsyntax.Node) hcl.Diagnostics {
c.setMax(typeString(node, c.depth))
c.depth++
if se, ok := node.(*hclsyntax.ScopeTraversalExpr); ok {
for _, t := range se.Traversal {
c.setMax(typeString(t, c.depth+1))
}
}
if se, ok := node.(*hclsyntax.RelativeTraversalExpr); ok {
for _, t := range se.Traversal {
c.setMax(typeString(t, c.depth+1))
}
}
return nil
}
func (c *counter) Exit(_ hclsyntax.Node) hcl.Diagnostics {
c.depth--
return nil
}
type writer struct {
source []byte
depth int
typeLength int
maxSourceLength int
}
func (w *writer) paddedString(s string) string {
s += strings.Repeat(" ", w.typeLength-len(s))
return s
}
func (w *writer) paddedTypeString(node any) string {
return w.paddedString(typeString(node, w.depth))
}
func (w *writer) formatCode(r hcl.Range) string {
b := r.SliceBytes(w.source)
src := string(b)
src = strings.ReplaceAll(src, "\n", " ")
src = strings.ReplaceAll(src, "\r", "")
src = strings.ReplaceAll(src, "\t", " ")
maxlen := w.maxSourceLength
if len(src) > maxlen {
half := maxlen/2 - 2
start := src[:half]
rest := src[half:]
rest = rest[len(rest)-half:]
src = start + " ... " + rest
}
return src
}
func (w *writer) WriteTraversal(traversal hcl.Traversal) {
w.depth++
for _, node := range traversal {
fmt.Printf("%s : %s\n", w.paddedTypeString(node), w.formatCode(node.SourceRange()))
}
w.depth--
}
func (w *writer) Enter(node hclsyntax.Node) hcl.Diagnostics {
lhs := w.paddedTypeString(node)
src := w.formatCode(node.Range())
fmt.Printf("%s : %s\n", lhs, src)
w.depth++
return nil
}
func (w *writer) Exit(node hclsyntax.Node) hcl.Diagnostics {
if se, ok := node.(*hclsyntax.ScopeTraversalExpr); ok {
w.WriteTraversal(se.Traversal)
}
if se, ok := node.(*hclsyntax.RelativeTraversalExpr); ok {
w.WriteTraversal(se.Traversal)
}
w.depth--
return nil
}
func printASTOutput(b []byte, expr bool) error {
var e hclsyntax.Node
var f *hcl.File
var diags hcl.Diagnostics
if expr {
e, diags = hclsyntax.ParseExpression(b, "test.hcl", hcl.Pos{Line: 1, Column: 1})
} else {
f, diags = hclsyntax.ParseConfig(b, "test.hcl", hcl.Pos{Line: 1, Column: 1})
if f != nil {
e = f.Body.(*hclsyntax.Body)
}
}
if diags.HasErrors() {
log.Println("input has errors:", diags)
}
if e == nil {
return nil
}
c := &counter{}
_ = hclsyntax.Walk(e, c)
_ = hclsyntax.Walk(e, &writer{
source: b,
maxSourceLength: 60,
typeLength: c.maxLen,
})
return nil
}
// AddDumpASTCommand adds a sub-command to display the AST for HCL source.
func AddDumpASTCommand(root *cobra.Command) {
var code string
var expr bool
c := &cobra.Command{
Use: "ast",
Short: `display ast for an HCL expression read from stdin or option`,
}
root.AddCommand(c)
f := c.Flags()
f.StringVarP(&code, "code", "c", "", "code to evaluate, read from stdin if not supplied")
f.BoolVarP(&expr, "expr", "e", false, "treat input as HCL expression rather than a source file")
c.RunE = func(cmd *cobra.Command, args []string) error {
cmd.SilenceUsage = true
if code == "" {
_, _ = fmt.Fprintf(os.Stderr, "reading stdin for source...\n")
b, err := io.ReadAll(os.Stdin)
if err != nil {
return err
}
code = string(b)
}
return printASTOutput([]byte(code), expr)
}
}
package cmd
import (
"fmt"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/features/crds"
types "github.com/crossplane-contrib/function-hcl/function-hcl-ls/types/v1"
"github.com/spf13/cobra"
)
// AddDownloadCRDsCommand adds a sub-command to download CRD definitions from package images,
// using the offline section of the CRD sources metadata file.
func AddDownloadCRDsCommand(root *cobra.Command) {
c := &cobra.Command{
Use: "download-crds [<sources-file>]",
Short: "download CRDs to local cache from a crd-sources file",
}
root.AddCommand(c)
var deleteCache bool
f := c.Flags()
f.BoolVarP(&deleteCache, "delete-cache", "d", deleteCache, "delete cache dir before download")
c.RunE = func(c *cobra.Command, args []string) error {
if len(args) > 1 {
return fmt.Errorf("expected at most one argument")
}
file := types.StandardSourcesFile
if len(args) == 1 {
file = args[0]
}
c.SilenceUsage = true
return crds.Download(file, deleteCache)
}
}
// Package cmd provides sub-command implementations.
package cmd
import (
"context"
"fmt"
"log"
"os"
"os/signal"
"runtime"
"runtime/pprof"
"syscall"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langserver"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langserver/handlers"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/utils/logging"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
type serveParams struct {
port int
logFilePath string
logModules string
cpuProfile string
memProfile string
reqConcurrency int
stdio bool
}
// Version tracks the version of the command.
var Version string
// AddServeCommand adds the serve sub-command.
func AddServeCommand(root *cobra.Command) {
var p serveParams
c := &cobra.Command{
Use: "serve",
Short: "run the language server",
RunE: func(cmd *cobra.Command, args []string) error {
cmd.SilenceUsage = true
return serve(p)
},
}
root.AddCommand(c)
f := c.Flags()
f.IntVar(&p.port, "port", 0, "port number to listen on (turns server into TCP mode)")
f.StringVar(&p.logFilePath, "log-file", "", "path to a file to log into with support "+
"for variables (e.g. timestamp, pid, ppid) via Go template syntax {{varName}}")
f.StringVar(&p.logModules, "log-modules", "", "comma-separated list of modules to enable logging for "+
"(langserver,handlers,eventbus,modules,crds,filesystem,docstore,perf) or 'all'")
f.StringVar(&p.cpuProfile, "cpu-profile", "", "file into which to write CPU profile")
f.StringVar(&p.memProfile, "mem-profile", "", "file into which to write memory profile")
f.IntVar(&p.reqConcurrency, "concurrency", 0, fmt.Sprintf("number of RPC requests to process concurrently,"+
" defaults to %d, concurrency lower than 2 is not recommended", langserver.DefaultConcurrency()))
f.BoolVar(&p.stdio, "stdio", true, "use stdio")
}
func serve(c serveParams) error {
if c.cpuProfile != "" {
stop, err := writeCpuProfileInto(c.cpuProfile)
if err != nil {
return errors.Wrap(err, "write CPU profile")
}
if stop != nil {
defer func() { _ = stop() }()
}
}
if c.memProfile != "" {
defer func() {
_ = writeMemoryProfileInto(c.memProfile)
}()
}
// Parse enabled modules
enabledModules, err := logging.ParseModules(c.logModules)
if err != nil {
return errors.Wrap(err, "parse log modules")
}
// Set up logging output and initialize registry
var logOutput *logging.FileLogger
if c.logFilePath != "" {
var err error
logOutput, err = logging.NewFileLogger(c.logFilePath)
if err != nil {
return errors.Wrap(err, "open log file")
}
defer func() { _ = logOutput.Close() }()
logging.Init(logOutput.Writer(), enabledModules)
} else {
logging.Init(nil, enabledModules)
}
// Get a logger for startup messages
logger := logging.LoggerFor(logging.ModuleLangServer)
ctx, cancelFunc := withSignalCancel(context.Background(), logger, os.Interrupt, syscall.SIGTERM)
defer cancelFunc()
logger.Printf("Starting function-hcl-ls %s", Version)
srv := langserver.New(ctx, langserver.Options{
ServerVersion: Version,
Concurrency: c.reqConcurrency,
Factory: handlers.NewSession,
})
if c.port != 0 {
err := srv.StartTCP(fmt.Sprintf("localhost:%d", c.port))
if err != nil {
return errors.Wrap(err, "start tcp server")
}
return nil
}
return srv.StartAndWait(os.Stdin, os.Stdout)
}
type stopFunc func() error
func withSignalCancel(ctx context.Context, l *log.Logger, sigs ...os.Signal) (
context.Context, context.CancelFunc,
) {
ctx, cancelFunc := context.WithCancel(ctx)
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, sigs...)
go func() {
select {
case sig := <-sigChan:
l.Printf("Cancellation signal (%s) received", sig)
cancelFunc()
case <-ctx.Done():
}
}()
f := func() {
signal.Stop(sigChan)
cancelFunc()
}
return ctx, f
}
func writeCpuProfileInto(rawPath string) (stopFunc, error) {
path, err := logging.ParseRawPath("cpuprofile-path", rawPath)
if err != nil {
return nil, err
}
f, err := os.Create(path)
if err != nil {
return nil, fmt.Errorf("could not create CPU profile: %s", err)
}
if err := pprof.StartCPUProfile(f); err != nil {
return f.Close, fmt.Errorf("could not start CPU profile: %s", err)
}
return func() error {
pprof.StopCPUProfile()
return f.Close()
}, nil
}
func writeMemoryProfileInto(rawPath string) error {
path, err := logging.ParseRawPath("memprofile-path", rawPath)
if err != nil {
return err
}
f, err := os.Create(path)
if err != nil {
return fmt.Errorf("could not create memory profile: %s", err)
}
defer func() { _ = f.Close() }()
runtime.GC()
if err := pprof.WriteHeapProfile(f); err != nil {
return fmt.Errorf("could not write memory profile: %s", err)
}
return nil
}
package cmd
import (
"encoding/json"
"fmt"
"runtime/debug"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
type versionOutput struct {
Version string `json:"version"`
buildInfo
}
type buildInfo struct {
GoVersion string `json:"go,omitempty"`
GoOS string `json:"os,omitempty"`
GoArch string `json:"arch,omitempty"`
Compiler string `json:"compiler,omitempty"`
}
func showVersion(jsonOutput bool) error {
info, ok := debug.ReadBuildInfo()
output := versionOutput{
Version: Version,
buildInfo: buildInfo{
GoVersion: "unknown",
GoOS: "unknown",
GoArch: "unknown",
Compiler: "unknown",
},
}
if ok {
output.GoVersion = info.GoVersion
for _, setting := range info.Settings {
// Filter for VCS-related info
switch setting.Key {
case "GOOS":
output.GoOS = setting.Value
case "GOARCH":
output.GoArch = setting.Value
case "compiler":
output.Compiler = setting.Value
}
}
}
if jsonOutput {
out, err := json.MarshalIndent(output, "", " ")
if err != nil {
return errors.Wrap(err, "marshal version output")
}
fmt.Println(string(out))
return nil
}
fmt.Printf("%s\nplatform: %s/%s\ngo: %s\ncompiler: %s\n",
Version, output.GoOS, output.GoArch, output.GoVersion, output.Compiler)
return nil
}
// AddVersionCommand adds a sub-command to display version information.
func AddVersionCommand(root *cobra.Command) {
var jsonOutput bool
c := &cobra.Command{
Use: "version",
Short: `display version and build information`,
}
root.AddCommand(c)
f := c.Flags()
f.BoolVar(&jsonOutput, "json", false, "output the version information as a JSON object")
c.RunE = func(cmd *cobra.Command, args []string) error {
cmd.SilenceUsage = true
return showVersion(jsonOutput)
}
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package document
import (
"bytes"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/document/source"
)
// Change represents an edit where a specific range in a document is substituted with specific text.
// When no range is present, a full document change is assumed.
type Change interface {
Text() string // the text to use
Range() *Range // the range to replace
}
// Changes is a list of change commands.
type Changes []Change
// ApplyChanges applies the supplied changes to the text supplied.
func ApplyChanges(original []byte, changes Changes) ([]byte, error) {
if len(changes) == 0 {
return original, nil
}
var buf bytes.Buffer
_, err := buf.Write(original)
if err != nil {
return nil, err
}
for _, ch := range changes {
err := applyDocumentChange(&buf, ch)
if err != nil {
return nil, err
}
}
return buf.Bytes(), nil
}
func applyDocumentChange(buf *bytes.Buffer, change Change) error {
// if the range is nil, we assume it is full content change
if change.Range() == nil {
buf.Reset()
_, err := buf.WriteString(change.Text())
return err
}
lines := source.MakeSourceLines("", buf.Bytes())
startByte, err := ByteOffsetForPos(lines, change.Range().Start)
if err != nil {
return err
}
endByte, err := ByteOffsetForPos(lines, change.Range().End)
if err != nil {
return err
}
diff := endByte - startByte
if diff > 0 {
buf.Grow(diff)
}
beforeChange := make([]byte, startByte)
copy(beforeChange, buf.Bytes())
afterBytes := buf.Bytes()[endByte:]
afterChange := make([]byte, len(afterBytes))
copy(afterChange, afterBytes)
buf.Reset()
_, err = buf.Write(beforeChange)
if err != nil {
return err
}
_, err = buf.WriteString(change.Text())
if err != nil {
return err
}
_, err = buf.Write(afterChange)
if err != nil {
return err
}
return nil
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
// Package diff provides facilities to represent the differences between two document texts
// as a sequence of document.Change records.
package diff
import (
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/document"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/document/source"
"github.com/hashicorp/hcl/v2"
"github.com/pmezard/go-difflib/difflib"
)
type fileChange struct {
newText string
rng *hcl.Range
opCode difflib.OpCode
}
func (ch *fileChange) Text() string {
return ch.newText
}
func (ch *fileChange) Range() *document.Range {
if ch.rng == nil {
return nil
}
return &document.Range{
Start: document.Pos{
Line: ch.rng.Start.Line - 1,
Column: ch.rng.Start.Column - 1,
},
End: document.Pos{
Line: ch.rng.End.Line - 1,
Column: ch.rng.End.Column - 1,
},
}
}
const (
opReplace = 'r'
opDelete = 'd'
opInsert = 'i'
opEqual = 'e'
)
// Diff calculates difference between the document's content
// and after byte sequence and returns it as document.Changes
func Diff(f document.Handle, before, after []byte) document.Changes {
return diffLines(f.Filename,
source.MakeSourceLines(f.Filename, before),
source.MakeSourceLines(f.Filename, after))
}
// diffLines calculates difference between two source.Lines
// and returns them as document.Changes
func diffLines(filename string, beforeLines, afterLines []source.Line) document.Changes {
context := 3
m := difflib.NewMatcher(
source.StringLines(beforeLines),
source.StringLines(afterLines))
changes := make(document.Changes, 0)
for _, group := range m.GetGroupedOpCodes(context) {
for _, c := range group {
if c.Tag == opEqual {
continue
}
// lines to pick from the original document (to delete/replace/insert to)
beforeStart, beforeEnd := c.I1, c.I2
// lines to pick from the new document (to replace ^ with)
afterStart, afterEnd := c.J1, c.J2
if c.Tag == opReplace {
var rng *hcl.Range
var newBytes []byte
for i, line := range beforeLines[beforeStart:beforeEnd] {
if i == 0 {
lr := line.Range
rng = &lr
continue
}
rng.End = line.Range.End
}
for _, line := range afterLines[afterStart:afterEnd] {
newBytes = append(newBytes, line.Bytes...)
}
changes = append(changes, &fileChange{
newText: string(newBytes),
rng: rng,
})
continue
}
if c.Tag == opDelete {
var deleteRng *hcl.Range
for i, line := range beforeLines[beforeStart:beforeEnd] {
if i == 0 {
lr := line.Range
deleteRng = &lr
continue
}
deleteRng.End = line.Range.End
}
changes = append(changes, &fileChange{
newText: "",
rng: deleteRng,
opCode: c,
})
continue
}
if c.Tag == opInsert {
insertRng := &hcl.Range{
Filename: filename,
Start: hcl.InitialPos,
End: hcl.InitialPos,
}
if beforeStart == beforeEnd {
line := beforeLines[beforeStart]
insertRng = line.Range.Ptr()
// We're inserting to the beginning of the line
// which we represent as 0-length range in HCL
insertRng.End = insertRng.Start
} else {
for i, line := range beforeLines[beforeStart:beforeEnd] {
if i == 0 {
insertRng = line.Range.Ptr()
continue
}
insertRng.End = line.Range.End
}
}
var newBytes []byte
for _, line := range afterLines[afterStart:afterEnd] {
newBytes = append(newBytes, line.Bytes...)
}
changes = append(changes, &fileChange{
newText: string(newBytes),
rng: insertRng,
})
continue
}
}
}
return changes
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package document
import (
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/utils/uri"
)
// DirHandle represents a directory location
//
// This may be received via LSP from the client (as URI)
// or constructed from a file path on OS FS.
type DirHandle struct {
URI string
}
// Path returns (the usually) absolute path for the dir handle.
func (dh DirHandle) Path() string {
return uri.MustPathFromURI(dh.URI)
}
// DirHandleFromPath creates a DirHandle from a given path.
//
// dirPath is expected to be a directory path (rather than document).
// It is however outside the scope of the function to verify
// this is actually the case or whether the directory exists.
func DirHandleFromPath(dirPath string) DirHandle {
return DirHandle{
URI: uri.FromPath(dirPath),
}
}
// DirHandleFromURI creates a DirHandle from a given URI.
//
// dirUri is expected to be a directory URI (rather than document).
// It is however outside the scope of the function to verify
// this is actually the case or whether the directory exists.
func DirHandleFromURI(dirUri string) DirHandle {
// Normalize the raw URI to account for any escaping differences
dirUri = uri.MustParseURI(dirUri)
return DirHandle{
URI: dirUri,
}
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
// Package document encapsulates processing for HCL text documents.
package document
import (
"path/filepath"
"time"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/document/source"
)
// Document represents a text document of interest.
type Document struct {
Dir DirHandle // the directory where the doc lives.
Filename string // the file name.
ModTime time.Time // last modified time.
LanguageID string // language ID as supplied by the language client.
Version int // document version used to ensure edits are in sequence.
Text []byte // the text of the document as a byte slice.
Lines []source.Line // text separated into lines to enable byte offset computation for position conversions.
}
// FullPath returns the full filesystem path of the document.
func (d *Document) FullPath() string {
return filepath.Join(d.Dir.Path(), d.Filename)
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package document
import (
"fmt"
)
type invalidPosErr struct {
Pos Pos
}
func (e *invalidPosErr) Error() string {
return fmt.Sprintf("invalid position: %s", e.Pos)
}
// NotFound returns an error that represents a missing document.
func NotFound(uri string) error {
return ¬Found{URI: uri}
}
// IsNotFound returns true if the supplied error refers to a missing document.
func IsNotFound(err error) bool {
_, ok := err.(*notFound)
return ok
}
type notFound struct {
URI string
}
func (e *notFound) Error() string {
msg := "document not found"
if e.URI != "" {
return fmt.Sprintf("%s: %s", e.URI, msg)
}
return msg
}
func (e *notFound) Is(err error) bool {
_, ok := err.(*notFound)
return ok
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package document
import (
"fmt"
"os"
"path"
"path/filepath"
"strings"
)
// Handle represents a document location
//
// This may be received via LSP from the client (as URI)
// or constructed from a file path on OS FS.
type Handle struct {
Dir DirHandle // the directory handle.
Filename string // the file name.
}
// HandleFromURI creates a Handle from a given URI.
//
// docURI is expected to be a document URI (rather than dir).
// It is however outside the scope of the function to verify
// this is actually the case or whether the file exists.
func HandleFromURI(docUri string) Handle {
filename := path.Base(docUri)
dirUri := strings.TrimSuffix(docUri, "/"+filename)
return Handle{
Dir: DirHandleFromURI(dirUri),
Filename: filename,
}
}
// HandleFromPath creates a Handle from a given path.
//
// docPath is expected to be a document path (rather than dir).
// It is however outside the scope of the function to verify
// this is actually the case or whether the file exists.
func HandleFromPath(docPath string) Handle {
filename := filepath.Base(docPath)
dirPath := strings.TrimSuffix(docPath, fmt.Sprintf("%c%s", os.PathSeparator, filename))
return Handle{
Dir: DirHandleFromPath(dirPath),
Filename: filename,
}
}
// FullPath returns the full filesystem path for the handle.
func (h Handle) FullPath() string {
return filepath.Join(h.Dir.Path(), h.Filename)
}
// FullURI returns a URI for the handle.
func (h Handle) FullURI() string {
return h.Dir.URI + "/" + h.Filename
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package document
import (
"unicode/utf16"
"unicode/utf8"
"github.com/apparentlymart/go-textseg/textseg"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/document/source"
)
// ByteOffsetForPos returns the byte offset for the supplied position or an error
// if such a position could not be found in the supplied lines.
func ByteOffsetForPos(lines []source.Line, pos Pos) (int, error) {
if pos.Line+1 > len(lines) {
return 0, &invalidPosErr{Pos: pos}
}
return byteOffsetForLSPColumn(lines[pos.Line], pos.Column), nil
}
// byteForLSPColumn takes an lsp.Position.Character value for the receiving line
// and finds the byte offset of the start of the UTF-8 sequence that represents
// it in the overall source buffer. This is different from the byte returned
// by posForLSPColumn because it can return offsets that are partway through
// a grapheme cluster, while HCL positions always round to the nearest
// grapheme cluster.
//
// Note that even this can't produce an exact result; if the column index
// refers to the second unit of a UTF-16 surrogate pair then it is rounded
// down the first unit because UTF-8 sequences are not divisible in the same
// way.
func byteOffsetForLSPColumn(l source.Line, lspCol int) int {
if lspCol < 0 {
return l.Range.Start.Byte
}
// Normally ASCII-only lines could be short-circuited here
// but it's not as easy to tell whether a line is ASCII-only
// based on column/byte differences as we also scan newlines
// and a single line range technically spans 2 lines.
// If there are non-ASCII characters then we need to edge carefully
// along the line while counting UTF-16 code units in our UTF-8 buffer,
// since LSP columns are a count of UTF-16 units.
byteCt := 0
utf16Ct := 0
colIdx := 1
remain := l.Bytes
for {
if len(remain) == 0 { // ran out of characters on the line, so given column is invalid
return l.Range.End.Byte
}
if utf16Ct >= lspCol { // we've found it
return l.Range.Start.Byte + byteCt
}
// Unlike our other conversion functions we're intentionally using
// individual UTF-8 sequences here rather than grapheme clusters because
// an LSP position might point into the middle of a grapheme cluster.
adv, chBytes, _ := textseg.ScanUTF8Sequences(remain, true)
remain = remain[adv:]
byteCt += adv
colIdx++
for len(chBytes) > 0 {
r, l := utf8.DecodeRune(chBytes)
chBytes = chBytes[l:]
c1, c2 := utf16.EncodeRune(r)
if c1 == 0xfffd && c2 == 0xfffd {
utf16Ct++ // codepoint fits in one 16-bit unit
} else {
utf16Ct += 2 // codepoint requires a surrogate pair
}
}
}
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package document
import "fmt"
// Range represents LSP-style range between two positions.
// Positions are zero-indexed unlike HCL ranges.
type Range struct {
Start, End Pos
}
// Pos represents LSP-style position (zero-indexed).
type Pos struct {
Line, Column int
}
func (p Pos) String() string {
return fmt.Sprintf("%d:%d", p.Line, p.Column)
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
// Package source provides facilities to represent text documents as a sequence
// of lines.
package source
import (
"bytes"
"github.com/hashicorp/hcl/v2"
)
// Line represents a line of source code. Each line is associated with an HCL range that
// includes the filename, start and end positions.
type Line struct {
// Bytes returns the line byte inc. any trailing end-of-line markers
Bytes []byte
// Range returns range of the line bytes inc. any trailing end-of-line markers
// The range will span across two lines in most cases
// (other than last line without trailing new line)
Range hcl.Range
}
// MakeSourceLines returns the lines in the supplied text. The returned lines will
// have one more extra line than the source for insertion use.
func MakeSourceLines(filename string, s []byte) []Line {
var ret []Line
lastRng := hcl.Range{
Filename: filename,
Start: hcl.InitialPos,
End: hcl.InitialPos,
}
sc := hcl.NewRangeScanner(s, filename, scanLines)
for sc.Scan() {
ret = append(ret, Line{
Bytes: sc.Bytes(),
Range: sc.Range(),
})
lastRng = sc.Range()
}
// Account for the last (virtual) user-perceived line
ret = append(ret, Line{
Bytes: []byte{},
Range: hcl.Range{
Filename: lastRng.Filename,
Start: lastRng.End,
End: lastRng.End,
},
})
return ret
}
// scanLines is a split function for a Scanner that returns each line of
// text (separated by \n), INCLUDING any trailing end-of-line marker.
// The last non-empty line of input will be returned even if it has no
// newline.
func scanLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
if atEOF && len(data) == 0 {
return 0, nil, nil
}
if i := bytes.IndexByte(data, '\n'); i >= 0 {
// We have a full newline-terminated line.
return i + 1, data[0 : i+1], nil
}
// If we're at EOF, we have a final, non-terminated line. Return it.
if atEOF {
return len(data), data, nil
}
// Request more data.
return 0, nil, nil
}
// StringLines returns the supplied lines as a sequence of strings.
func StringLines(lines []Line) []string {
strLines := make([]string, len(lines))
for i, l := range lines {
strLines[i] = string(l.Bytes)
}
return strLines
}
// Package store implements a store for HCL documents open in the editor and tracks unsaved changes for each of them.
package store
import (
"fmt"
"log"
"sync"
"time"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/document"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/document/source"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/utils/logging"
)
// Store is the document store.
type Store struct {
l sync.RWMutex
docs map[document.Handle]*document.Document
logger *log.Logger
}
// New creates an empty store.
func New() *Store {
return &Store{
docs: map[document.Handle]*document.Document{},
logger: logging.LoggerFor(logging.ModuleDocStore),
}
}
// Open creates an in-memory representation of the supplied document. The document should not
// have been already opened.
func (s *Store) Open(dh document.Handle, langId string, version int, text []byte) error {
s.l.Lock()
defer s.l.Unlock()
doc := s.docs[dh]
if doc != nil {
return fmt.Errorf("document %s already exists", dh.FullPath())
}
s.docs[dh] = &document.Document{
Dir: dh.Dir,
Filename: dh.Filename,
ModTime: time.Now().UTC(),
LanguageID: langId,
Version: version,
Text: text,
Lines: source.MakeSourceLines(dh.Filename, text),
}
return nil
}
// Update modifies the store to update document text.
func (s *Store) Update(dh document.Handle, newText []byte, newVersion int) error {
s.l.Lock()
defer s.l.Unlock()
doc := s.docs[dh]
if doc == nil {
return document.NotFound(dh.FullPath())
}
if newVersion <= doc.Version {
return fmt.Errorf("version not ascending: %d => %d", doc.Version, newVersion)
}
s.docs[dh] = &document.Document{
Dir: dh.Dir,
Filename: dh.Filename,
ModTime: time.Now().UTC(),
LanguageID: doc.LanguageID,
Version: newVersion,
Text: newText,
Lines: source.MakeSourceLines(dh.Filename, newText),
}
return nil
}
// Close removes the supplied document from the store.
func (s *Store) Close(dh document.Handle) error {
s.l.Lock()
defer s.l.Unlock()
doc := s.docs[dh]
if doc == nil {
return document.NotFound(dh.FullPath())
}
delete(s.docs, dh)
return nil
}
// HasOpenDocuments returns true if the supplied directory has any open documents.
func (s *Store) HasOpenDocuments(dirHandle document.DirHandle) bool {
s.l.RLock()
defer s.l.RUnlock()
for key := range s.docs {
if key.Dir == dirHandle {
return true
}
}
return false
}
// IsDocumentOpen returns true if it has been opened in the store.
func (s *Store) IsDocumentOpen(dh document.Handle) bool {
s.l.RLock()
defer s.l.RUnlock()
return s.docs[dh] != nil
}
// Get returns the document for the supplied handle or an error if the document
// could not be found.
func (s *Store) Get(dh document.Handle) (*document.Document, error) {
s.l.RLock()
defer s.l.RUnlock()
d := s.docs[dh]
if d == nil {
return nil, document.NotFound(dh.FullPath())
}
return d, nil
}
// List returns all documents under the specified directory.
func (s *Store) List(dirHandle document.DirHandle) []*document.Document {
s.l.RLock()
defer s.l.RUnlock()
var ret []*document.Document
for key, doc := range s.docs {
if key.Dir == dirHandle {
ret = append(ret, doc)
}
}
return ret
}
// Package eventbus provides facilities for asynchronous processing of events
// mediated by a type-safe bus.
package eventbus
import (
"log"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/document"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langserver/protocol"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/utils/logging"
"github.com/hashicorp/hcl/v2"
)
// EditEvent is an event to signal that a file in a directory has changed.
//
// It is usually emitted when a document is changed via a language server
// text edit event.
type EditEvent struct {
Doc document.Handle
LanguageID string
}
// ChangeWatchEvent is the event that is emitted when a client notifies
// the language server that a directory or file was changed outside the
// editor.
type ChangeWatchEvent struct {
// RawPath contains an OS specific path to the file or directory that was
// changed. Usually extracted from the URI.
RawPath string
// IsDir is true if we were able to determine that the path is a directory.
// This is not set for delete events.
IsDir bool
// ChangeType specifies if the file or directory was created, updated, or deleted.
ChangeType protocol.FileChangeType
}
// OpenEvent is an event to signal that a document is open in the editor.
//
// It is usually emitted when a document is opened via a language server
// text synchronization event.
type OpenEvent struct {
Doc document.Handle
LanguageID string
}
// DiagnosticsEvent is an event to signal that diagnostics are available for a file.
//
// It is emitted after parsing a file to notify the language server
// to publish diagnostics to the client.
type DiagnosticsEvent struct {
Doc document.Handle
Diags hcl.Diagnostics
}
// NoCRDSourcesEvent is an event to signal that no CRD sources were found
// for a workspace directory. This is used to prompt the user to configure
// CRD sources.
type NoCRDSourcesEvent struct {
Dir string // workspace directory with no CRD config
}
// EventBus is a simple event bus that allows for subscribing to and publishing
// events of a specific type.
//
// It has a static list of topics. Each topic can have multiple subscribers.
// When an event is published to a topic, it is sent to all subscribers.
type EventBus struct {
logger *log.Logger
openTopic *topic[OpenEvent]
changeTopic *topic[EditEvent]
changeWatchTopic *topic[ChangeWatchEvent]
diagnosticsTopic *topic[DiagnosticsEvent]
noCRDSourcesTopic *topic[NoCRDSourcesEvent]
}
// New creates an event bus.
func New() *EventBus {
return &EventBus{
logger: logging.LoggerFor(logging.ModuleEventBus),
openTopic: newTopic[OpenEvent](),
changeTopic: newTopic[EditEvent](),
changeWatchTopic: newTopic[ChangeWatchEvent](),
diagnosticsTopic: newTopic[DiagnosticsEvent](),
noCRDSourcesTopic: newTopic[NoCRDSourcesEvent](),
}
}
// PublishOpenEvent publishes a document open event.
func (b *EventBus) PublishOpenEvent(e OpenEvent) {
b.logger.Printf("bus: -> publish open event %s %s", e.Doc.Dir.Path(), e.Doc.Filename)
b.openTopic.publish(e)
}
// SubscribeToOpenEvents adds a subscriber to process a document open event.
func (b *EventBus) SubscribeToOpenEvents(identifier string) <-chan OpenEvent {
b.logger.Printf("bus: %q subscribe to open events", identifier)
return b.openTopic.subscribe(identifier)
}
// PublishEditEvent publishes a document edit event.
func (b *EventBus) PublishEditEvent(e EditEvent) {
b.logger.Printf("bus: -> publish change event %s %s", e.Doc.Dir.Path(), e.Doc.Filename)
b.changeTopic.publish(e)
}
// SubscribeToEditEvents adds a subscriber to process document edit events.
func (b *EventBus) SubscribeToEditEvents(identifier string) <-chan EditEvent {
b.logger.Printf("bus: %q subscribed to change events", identifier)
return b.changeTopic.subscribe(identifier)
}
// PublishChangeWatchEvent publishes a change watch event.
func (b *EventBus) PublishChangeWatchEvent(e ChangeWatchEvent) {
b.logger.Printf("bus: -> publish change watch event %s", e.RawPath)
b.changeWatchTopic.publish(e)
}
// SubscribeToChangeWatchEvents adds a subscriber to process change watch events.
func (b *EventBus) SubscribeToChangeWatchEvents(identifier string) <-chan ChangeWatchEvent {
b.logger.Printf("bus: %q subscribed to change watch events", identifier)
return b.changeWatchTopic.subscribe(identifier)
}
// PublishDiagnosticsEvent publishes a diagnostics event.
func (b *EventBus) PublishDiagnosticsEvent(e DiagnosticsEvent) {
b.logger.Printf("bus: -> publish diagnostics event %s %s (%d diags)", e.Doc.Dir.Path(), e.Doc.Filename, len(e.Diags))
b.diagnosticsTopic.publish(e)
}
// SubscribeToDiagnosticsEvents adds a subscriber to process diagnostics events.
func (b *EventBus) SubscribeToDiagnosticsEvents(identifier string) <-chan DiagnosticsEvent {
b.logger.Printf("bus: %q subscribed to diagnostics events", identifier)
return b.diagnosticsTopic.subscribe(identifier)
}
// PublishNoCRDSourcesEvent publishes a no CRD sources event.
func (b *EventBus) PublishNoCRDSourcesEvent(e NoCRDSourcesEvent) {
b.logger.Printf("bus: -> publish no CRD sources event %s", e.Dir)
b.noCRDSourcesTopic.publish(e)
}
// SubscribeToNoCRDSourcesEvents adds a subscriber to process no CRD sources events.
func (b *EventBus) SubscribeToNoCRDSourcesEvents(identifier string) <-chan NoCRDSourcesEvent {
b.logger.Printf("bus: %q subscribed to no CRD sources events", identifier)
return b.noCRDSourcesTopic.subscribe(identifier)
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package eventbus
import (
"slices"
"sync"
)
const channelSize = 10
// topic represents a generic subscription topic
type topic[T any] struct {
l sync.Mutex
subscribers []subscriber[T]
}
// subscriber represents a subscriber to a topic
type subscriber[T any] struct {
identifier string
// channel is the channel to which all events of the topic are sent
channel chan<- T
}
// newTopic creates a new topic
func newTopic[T any]() *topic[T] {
return &topic[T]{}
}
// subscribe adds a subscriber to a topic
func (eb *topic[T]) subscribe(identifier string) <-chan T {
channel := make(chan T, channelSize)
eb.l.Lock()
defer eb.l.Unlock()
eb.subscribers = append(eb.subscribers, subscriber[T]{
identifier: identifier,
channel: channel,
})
return channel
}
// publish sends an event to all subscribers of a specific topic
func (eb *topic[T]) publish(event T) {
eb.l.Lock()
subscribers := slices.Clone(eb.subscribers)
eb.l.Unlock()
for _, s := range subscribers {
s.channel <- event
}
}
// Package crds provides schema information for CRDs and XRDs that are used in function-hcl compositions.
// For each open document, it tries and discovers CRD information that the user has captured in a set of files
// and provides dynamic schemas for these types.
package crds
import (
"context"
"log"
"sync/atomic"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/eventbus"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/features/crds/store"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/resource"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/utils/logging"
)
// Config configures the feature.
type Config struct {
EventBus *eventbus.EventBus
}
// CRDs provides schemas for CRDs and XRDs.
type CRDs struct {
bus *eventbus.EventBus
store *store.Store
logger *log.Logger
schemas atomic.Pointer[resource.Schemas]
}
// Download processes the offline source definition and downloads a list
// of images on to the filesystem.
func Download(sourcesFile string, deleteCache bool) error {
return downloadCRDs(sourcesFile, deleteCache)
}
// New creates an instance of the CRD discovery feature.
func New(c Config) *CRDs {
ret := &CRDs{
bus: c.EventBus,
logger: logging.LoggerFor(logging.ModuleCRDs),
}
ret.store = store.New(func(dir string) {
c.EventBus.PublishNoCRDSourcesEvent(eventbus.NoCRDSourcesEvent{Dir: dir})
})
ret.schemas.Store(resource.ToSchemas())
return ret
}
// Start starts background event processing. The background routine terminates
// when the context is canceled.
func (c *CRDs) Start(ctx context.Context) {
c.start(ctx)
}
// DynamicSchemas returns the schemas loaded for the supplied module directory path.
func (c *CRDs) DynamicSchemas(path string) *resource.Schemas {
return c.store.GetSchema(path)
}
package crds
import (
"fmt"
"io"
"log"
"os"
"path"
"path/filepath"
"strings"
"time"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/resource/loader"
types "github.com/crossplane-contrib/function-hcl/function-hcl-ls/types/v1"
"github.com/ghodss/yaml"
"github.com/google/go-containerregistry/pkg/name"
"github.com/pkg/errors"
"k8s.io/apimachinery/pkg/runtime"
)
func writeCacheFile(dir string, image string, objects []runtime.Object) (filename string, finalErr error) {
ref, err := name.ParseReference(image, name.WithDefaultTag("latest"))
if err != nil {
return "", fmt.Errorf("invalid image reference %q: %w", image, err)
}
var (
repo string
version string
)
switch r := ref.(type) {
case name.Tag:
// e.g. gcr.io/repo/img:tag
repo = r.RepositoryStr()
version = r.TagStr()
case name.Digest:
// e.g. gcr.io/repo/img@sha256:abcdef...
repo = r.RepositoryStr()
// Replace ':' in digest to keep filename filesystem-safe.
version = r.DigestStr()
default:
repo = ref.Context().RepositoryStr()
version = ref.Identifier()
}
base := path.Base(repo)
version = strings.ReplaceAll(version, ":", "-")
file := filepath.Join(dir, fmt.Sprintf("%s-%s.yaml", base, version))
f, err := os.Create(file)
if err != nil {
return file, err
}
defer func() {
err := f.Close()
if err != nil && finalErr == nil {
finalErr = err
}
}()
_, _ = io.WriteString(f, strings.TrimSpace(fmt.Sprintf(`
# GENERATED FILE DO NOT EDIT
# CRDs and XRDs downloaded from %s
`, image)))
_, _ = io.WriteString(f, "\n")
for i, obj := range objects {
if i > 0 {
if _, err = io.WriteString(f, "\n---\n"); err != nil {
return file, err
}
}
var b []byte
b, err = yaml.Marshal(obj)
if err != nil {
return file, err
}
if _, err = f.Write(b); err != nil {
return file, err
}
}
return file, nil
}
func downloadCRDs(f string, deleteCache bool) (finalErr error) {
logger := log.New(os.Stderr, "", 0)
start := time.Now()
defer func() {
if finalErr == nil {
logger.Printf("completed in %s", time.Since(start).Round(time.Second))
}
}()
sourcesFile, err := filepath.Abs(f)
if err != nil {
return errors.Wrap(err, "get absolute path")
}
logger.Printf("* processing locations from: %s ...", sourcesFile)
src, err := readSource(sourcesFile)
if err != nil {
return err
}
cd := src.Offline.CacheDir
if cd == "" {
return fmt.Errorf("no cache dir specified in %s", sourcesFile)
}
if len(src.Offline.Images) == 0 {
return fmt.Errorf("no offline images specified in %s", sourcesFile)
}
baseDir := filepath.Dir(sourcesFile)
cacheDir := filepath.Clean(filepath.Join(baseDir, cd))
if deleteCache {
logger.Printf("* deleting cache dir: %s ...", cacheDir)
if err := os.RemoveAll(cacheDir); err != nil {
return err
}
}
if err := os.MkdirAll(cacheDir, 0o755); err != nil {
return err
}
logger.Printf("* writing CRDs to dir: %s ...", cacheDir)
logger.Printf("* downloading CRDs from %d images ...", len(src.Offline.Images))
for imageIndex, image := range src.Offline.Images {
l := loader.NewCrossplanePackage(image)
objects, err := l.ExtractObjects()
if err != nil {
return errors.Wrapf(err, "extract objects from %s", image)
}
outFile, err := writeCacheFile(cacheDir, image, objects)
if err != nil {
return errors.Wrapf(err, "write CRD file %s", outFile)
}
logger.Printf("\t%3d. %s (%d objects)", imageIndex+1, filepath.Base(outFile), len(objects))
}
return nil
}
func readSource(filename string) (*types.CRDSource, error) {
b, err := os.ReadFile(filename)
if err != nil {
return nil, err
}
var ret types.CRDSource
err = yaml.Unmarshal(b, &ret)
if err != nil {
return nil, errors.Wrap(err, "unmarshal CRD source")
}
return &ret, nil
}
package crds
import (
"context"
lsp "github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langserver/protocol"
)
func (c *CRDs) start(ctx context.Context) {
c.store.Start(ctx)
open := c.bus.SubscribeToOpenEvents("feature.crds")
changeWatch := c.bus.SubscribeToChangeWatchEvents("feature.crds")
go func() {
for {
var err error
select {
case event := <-open:
err = c.onOpen(event.Doc.Dir.Path())
case event := <-changeWatch:
err = c.onChangeWatch(event.ChangeType, event.RawPath, event.IsDir)
case <-ctx.Done():
c.logger.Print("stopped crds feature")
return
}
if err != nil {
c.logger.Printf("crds: process event: %q", err)
}
}
}()
}
func (c *CRDs) onOpen(path string) error {
c.store.RegisterOpenDir(path)
return nil
}
func (c *CRDs) onChangeWatch(changeType lsp.FileChangeType, path string, isDir bool) error {
c.logger.Printf("change watch event: type=%d path=%s isDir=%t", changeType, path, isDir)
switch {
case changeType == lsp.Deleted:
c.store.ProcessPathDeletion(path)
case isDir && changeType == lsp.Created: // no need to handle dir modification events, since some file would have changed
c.store.ProcessNewDir(path)
default:
c.store.ProcessFile(path)
}
return nil
}
// Package store provides a self-managing CRD store and implements background CRD discovery.
// It can provide the last known good state of a store related to a specific
// module at any time. Information can change in subsequent calls as more schemas are discovered.
package store
import (
"context"
"log"
"sync"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/resource"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/utils/queue"
)
// Store is the CRD store.
type Store struct {
queue *queue.Queue
files *fileStore
sources *sourceStore
seenLock sync.RWMutex
seenDirs map[string]string
onNoCRDSources func(dir string)
}
// New creates a new store. The onNoCRDSources callback is called when
// no CRD sources are found for a directory. It may be nil.
func New(onNoCRDSources func(dir string)) *Store {
return &Store{
queue: queue.New(1),
files: newFileStore(),
sources: newSourceStore(),
seenDirs: map[string]string{},
onNoCRDSources: onNoCRDSources,
}
}
// Start starts background processing of the store that ends when the
// supplied context is canceled.
func (s *Store) Start(ctx context.Context) {
s.queue.Start(ctx)
}
// RegisterOpenDir registers a module directory as a candidate for schema discovery.
func (s *Store) RegisterOpenDir(modulePath string) {
s.registerOpenDir(modulePath)
}
// GetSchema returns the known schema related to the specified module directory.
func (s *Store) GetSchema(modulePath string) *resource.Schemas {
s.seenLock.RLock()
defer s.seenLock.RUnlock()
sourcePath, ok := s.seenDirs[modulePath]
if !ok {
log.Println("internal error: directory", modulePath, "not registered")
return emptySchema
}
si := s.sources.get(sourcePath)
if si == nil {
log.Println("warn: source directory", sourcePath, "not found")
return emptySchema
}
return si.schema
}
// ProcessNewDir reprocesses the cache when a new directory is created, since it may
// be a new source root that the user introduced.
func (s *Store) ProcessNewDir(path string) {
s.reprocess() // TODO: make this more intelligent
}
// ProcessFile processes an added or changed file.
func (s *Store) ProcessFile(path string) {
if shouldProcessFile(path) {
s.reprocess()
}
}
// ProcessPathDeletion processes file or directory deletion events at the supplied path.
func (s *Store) ProcessPathDeletion(path string) {
s.reprocess() // TODO: make this more intelligent
}
package store
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"io/fs"
"os"
"sync"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/resource"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/resource/loader"
)
var errNoChanges = fmt.Errorf("no changes")
func fileInfoHash(fi fs.FileInfo) string {
h := sha256.New()
_, _ = fmt.Fprint(h, fi.ModTime().UnixNano())
_, _ = fmt.Fprint(h, fi.Size())
return hex.EncodeToString(h.Sum(nil))
}
// fileSchema tracks the schemas found in a single loaded file.
type fileSchema struct {
path string
checksum string
schema *resource.Schemas
}
func (f *fileSchema) Path() string {
return f.path
}
func (f *fileSchema) Checksum() string {
return f.checksum
}
func (f *fileSchema) Schema() *resource.Schemas {
return f.schema
}
// fileStore tracks all files that contain CRD information independent of which
// source it is used in.
type fileStore struct {
l sync.RWMutex
files map[string]*fileSchema
}
func newFileStore() *fileStore {
return &fileStore{
files: map[string]*fileSchema{},
}
}
func (f *fileStore) get(path string) *fileSchema {
f.l.RLock()
defer f.l.RUnlock()
return f.files[path]
}
func (f *fileStore) put(fs *fileSchema) {
f.l.Lock()
defer f.l.Unlock()
f.files[fs.path] = fs
}
//nolint:unused
func (f *fileStore) remove(filePath string) {
f.l.Lock()
defer f.l.Unlock()
delete(f.files, filePath)
}
//nolint:unused
func (f *fileStore) list() map[string]*fileSchema {
f.l.RLock()
defer f.l.RUnlock()
list := map[string]*fileSchema{}
for k, v := range f.files {
list[k] = v
}
return list
}
func (f *fileStore) add(filePath string) error {
have := f.get(filePath)
st, err := os.Stat(filePath)
if err != nil {
return err
}
hashString := fileInfoHash(st)
if have != nil && have.checksum == hashString {
return errNoChanges
}
schema, err := loader.NewFile(filePath).Load()
if err != nil {
return err
}
f.put(&fileSchema{
path: filePath,
checksum: hashString,
schema: schema,
})
return nil
}
func (f *fileStore) getSchema(path string) *resource.Schemas {
f.l.RLock()
defer f.l.RUnlock()
fi := f.files[path]
if fi == nil {
return emptySchema
}
return fi.Schema()
}
package store
import (
"sync"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/resource"
types "github.com/crossplane-contrib/function-hcl/function-hcl-ls/types/v1"
)
// sourceInfo tracks the set of files and the schema for a single CRD source
type sourceInfo struct {
sourcePath string
source *types.CRDSourceRuntime
expandedFiles map[string]bool
schema *resource.Schemas
}
func (s *sourceInfo) copy() *sourceInfo {
if s == nil {
return nil
}
m := make(map[string]bool)
for k, v := range s.expandedFiles {
m[k] = v
}
c := *s.source
return &sourceInfo{
sourcePath: s.sourcePath,
source: &c,
expandedFiles: m,
schema: s.schema,
}
}
type sourceStore struct {
l sync.RWMutex
sources map[string]*sourceInfo
}
func newSourceStore() *sourceStore {
return &sourceStore{
sources: map[string]*sourceInfo{},
}
}
func (s *sourceStore) get(path string) *sourceInfo {
s.l.RLock()
defer s.l.RUnlock()
return s.sources[path].copy()
}
func (s *sourceStore) put(ss *sourceInfo) {
s.l.Lock()
defer s.l.Unlock()
s.sources[ss.sourcePath] = ss.copy()
}
func (s *sourceStore) list() []string {
s.l.RLock()
defer s.l.RUnlock()
var stores []string
for k := range s.sources {
stores = append(stores, k)
}
return stores
}
package store
import (
"fmt"
"log"
"os"
"path/filepath"
"sort"
"github.com/bmatcuk/doublestar/v4"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/resource"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/utils/queue"
types "github.com/crossplane-contrib/function-hcl/function-hcl-ls/types/v1"
"github.com/ghodss/yaml"
"github.com/pkg/errors"
)
func shouldProcessFile(name string) bool {
return filepath.Ext(name) == ".yaml"
}
var emptySchema = resource.ToSchemas()
func discoveryKeyForRegisteredDir(dir string) queue.Key {
return queue.Key(fmt.Sprintf("dir-discovery:%s", dir))
}
func loadSourceKey(dir string) queue.Key {
return queue.Key(fmt.Sprintf("load-source:%s", dir))
}
func loadSchemaKey(filePath string) queue.Key {
return queue.Key(fmt.Sprintf("load-schema:%s", filePath))
}
func (s *Store) registerOpenDir(path string) {
s.seenLock.Lock()
defer s.seenLock.Unlock()
if _, ok := s.seenDirs[path]; ok {
return
}
s.seenDirs[path] = ""
s.queue.Enqueue(discoveryKeyForRegisteredDir(path), func() error {
return s.discoverSourceStore(path, false)
})
}
func (s *Store) reprocess() {
s.seenLock.Lock()
defer s.seenLock.Unlock()
for moduleDir := range s.seenDirs {
s.queue.Enqueue(discoveryKeyForRegisteredDir(moduleDir), func() error {
return s.discoverSourceStore(moduleDir, true)
})
}
}
func findAncestor(pathToSearch string, fileToFind string, expectDir bool) (string, bool) {
parent := filepath.Dir(pathToSearch)
if parent == pathToSearch {
return "", false
}
testPath := filepath.Join(pathToSearch, fileToFind)
st, err := os.Stat(testPath)
if err != nil {
return findAncestor(parent, fileToFind, expectDir)
}
if st.IsDir() != expectDir {
return findAncestor(parent, fileToFind, expectDir)
}
return pathToSearch, true
}
func (s *Store) discoverSourceStore(dir string, reprocessing bool) error {
foundDir, found := findAncestor(dir, types.StandardSourcesFile, false)
if !found {
foundDir, found = findAncestor(dir, types.DefaultSourcesDir, true)
}
if !found {
if !reprocessing && s.onNoCRDSources != nil {
s.onNoCRDSources(dir)
}
return nil
}
s.seenLock.Lock()
s.seenDirs[dir] = foundDir
defer s.seenLock.Unlock()
store := s.sources.get(foundDir)
if store == nil || reprocessing {
s.queue.Enqueue(loadSourceKey(foundDir), func() error {
return s.loadSourceStoreAt(foundDir)
})
}
return nil
}
func (s *Store) getSourceInfo(dir string) (ret types.CRDSource, _ error) {
var src types.CRDSource
filePath := filepath.Join(dir, types.StandardSourcesFile)
st, err := os.Stat(filePath)
if err == nil && !st.IsDir() {
sourceFilePath := filepath.Join(dir, types.StandardSourcesFile)
b, err := os.ReadFile(sourceFilePath)
if err != nil {
return ret, err
}
err = yaml.Unmarshal(b, &src)
if err != nil {
return ret, errors.Wrapf(err, "unmarshal source file %s", sourceFilePath)
}
return src, nil
}
filePath = filepath.Join(dir, types.DefaultSourcesDir)
_, err = os.Stat(filePath)
if err != nil {
return ret, fmt.Errorf("load source store at %s: no sources file or default directory", filePath)
}
return types.CRDSource{
Runtime: types.CRDSourceRuntime{
Scope: types.ScopeBoth,
Paths: []string{
filepath.Join(types.DefaultSourcesDir, "*.yaml"),
},
},
}, nil
}
func (s *Store) loadSourceStoreAt(dir string) error {
src, err := s.getSourceInfo(dir)
if err != nil {
return err
}
foundFiles := map[string]bool{}
for _, p := range src.Runtime.Paths {
var pattern string
if filepath.IsAbs(p) {
pattern = p
} else {
pattern = filepath.Clean(filepath.Join(dir, p))
}
matches, err := doublestar.FilepathGlob(pattern)
if err != nil {
log.Printf("filepath glob err, ignore: %s : %s", pattern, err)
continue
}
for _, match := range matches {
st, err := os.Stat(match)
if err != nil {
log.Printf("stat err, ignore: %s : %s", match, err)
continue
}
if st.IsDir() {
log.Printf("skip dir: %s", match)
continue
}
if !shouldProcessFile(match) {
log.Printf("skip non-yaml file: %s", match)
continue
}
foundFiles[match] = true
}
}
ss := s.sources.get(dir)
if ss == nil { // never before seen
ss = &sourceInfo{
sourcePath: dir,
source: &src.Runtime,
expandedFiles: foundFiles,
schema: emptySchema,
}
} else {
// but leave the last known schema for this one alone
ss.source = &src.Runtime
ss.expandedFiles = foundFiles
}
s.sources.put(ss)
var files []string
for ff := range foundFiles {
files = append(files, ff)
}
sort.Strings(files)
for _, file := range files {
s.queue.Enqueue(loadSchemaKey(file), func() error {
err := s.files.add(file)
if err != nil && !errors.Is(err, errNoChanges) {
return err
}
if errors.Is(err, errNoChanges) {
return nil
}
return s.propagateSchemaForFile(file)
})
}
return nil
}
func (s *Store) propagateSchemaForFile(file string) error {
stores := s.sources.list()
for _, storePath := range stores {
ss := s.sources.get(storePath)
if !ss.expandedFiles[file] {
continue
}
var schemas []*resource.Schemas
for f := range ss.expandedFiles {
schemas = append(schemas, s.files.getSchema(f))
}
updated := resource.Compose(schemas...).FilterScope(ss.source.Scope)
ss.schema = updated
s.sources.put(ss)
}
return nil
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
// Package modules provides facilities to track the parsed state of modules
// and provider completion and hover contexts, among other things.
package modules
import (
"context"
"io/fs"
"log"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/document"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/eventbus"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/features/modules/store"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/funchcl/decoder"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/funchcl/target"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/lang"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/schema"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/resource"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/utils/logging"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/utils/queue"
"github.com/hashicorp/hcl/v2"
)
const (
// XRDFile is a metadata file in the same directory as a module to discover
// the API version and kind of the composite resource such that this can
// provide intelligent completion for the `req.composite` external variable.
XRDFile = ".xrd.yaml"
)
// dependencies
// ReadOnlyFS is a read-only filesystem needed for module processing.
type ReadOnlyFS interface {
fs.FS // extends a base FS
ReadDir(name string) ([]fs.DirEntry, error) // allows listing directory contents.
ReadFile(name string) ([]byte, error) // convenience method to read a file.
Stat(name string) (fs.FileInfo, error) // provides file information
}
// DocStore provides minimal information about the state of the document store
// for efficient module processing.
type DocStore interface {
// HasOpenDocuments returns true if the supplied directory has documents open.
HasOpenDocuments(dirHandle document.DirHandle) bool
// IsDocumentOpen returns true if the supplied document is currently open.
IsDocumentOpen(dh document.Handle) bool
}
// DynamicSchemas provides schemas on demand for an API version, kind tuple.
// It can also list all such known tuples.
type DynamicSchemas interface {
Keys() []resource.Key // return all known keys
Schema(apiVersion, kind string) *schema.AttributeSchema // return schema for the supplied key
}
// DynamicSchemaProvider provides dynamic schema information for a module rooted at the
// supplied path.
type DynamicSchemaProvider func(modPath string) DynamicSchemas
// Modules groups everything related to modules.
// Its internal state keeps track of all modules in the workspace.
type Modules struct {
eventbus *eventbus.EventBus
queue *queue.Queue
docStore DocStore
fs ReadOnlyFS
provider DynamicSchemaProvider
store *store.Store
logger *log.Logger
}
// Config is the feature configuration.
type Config struct {
EventBus *eventbus.EventBus // event bus to listen on
DocStore DocStore // document store
FS ReadOnlyFS // filesystem that can provide unsaved document changes
Provider DynamicSchemaProvider // provider to get dynamic schemas for a module, for autocomplete
}
// New returns a new Modules instance.
func New(c Config) (*Modules, error) {
return &Modules{
eventbus: c.EventBus,
queue: queue.New(1),
docStore: c.DocStore,
fs: c.FS,
provider: c.Provider,
logger: logging.LoggerFor(logging.ModuleModules),
store: store.New(),
}, nil
}
// Start starts background activities. It terminates when the supplied context is closed.
func (m *Modules) Start(ctx context.Context) {
m.start(ctx)
}
// PathContext returns the context for the supplied path.
func (m *Modules) PathContext(p lang.Path) (decoder.Context, error) {
return m.pathContext(p)
}
// PathCompletionContext returns the completion/ hover context for the supplied path, for a given position in a
// specific file. This takes into account the variables that are visible from that position.
func (m *Modules) PathCompletionContext(p lang.Path, filename string, pos hcl.Pos) (decoder.CompletionContext, error) {
return m.pathCompletionContext(p, filename, pos)
}
// ReferenceMap returns the map of document references from declaration to references and vice-versa.
func (m *Modules) ReferenceMap(p lang.Path) (*target.ReferenceMap, error) {
return m.referenceMap(p)
}
// WaitUntilProcessed waits until all jobs currently queued for the supplied directory complete.
func (m *Modules) WaitUntilProcessed(dir string) {
m.queue.WaitForKey(queue.Key(dir))
}
package modules
import (
"fmt"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/funchcl/decoder"
ourschema "github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/funchcl/schema"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/funchcl/target"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/lang"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/schema"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
)
type pathCtx struct {
schema.Lookup
dir string
files map[string]*hcl.File
}
func (c *pathCtx) Dir() string {
return c.dir
}
func (c *pathCtx) Files() []string {
var ret []string
for file := range c.files {
ret = append(ret, file)
}
return ret
}
func (c *pathCtx) HCLFile(expr hcl.Expression) *hcl.File {
f, _ := c.HCLFileByName(expr.Range().Filename)
return f
}
func (c *pathCtx) HCLFileByName(name string) (*hcl.File, bool) {
f, ok := c.files[name]
return f, ok
}
func (c *pathCtx) FileBytes(e hcl.Expression) []byte {
b, _ := c.FileBytesByName(e.Range().Filename)
return b
}
func (c *pathCtx) FileBytesByName(name string) ([]byte, bool) {
f, ok := c.files[name]
if !ok {
return nil, false
}
return f.Bytes, true
}
func (c *pathCtx) Behavior() decoder.LangServerBehavior {
return decoder.GetBehavior()
}
type ctx struct {
pathCtx
completionFunctions map[string]decoder.CompletionFunc
targetSchema *schema.AttributeSchema
}
func (c *ctx) TargetSchema() *schema.AttributeSchema {
return c.targetSchema
}
func (c *ctx) CompletionFunc(hookName string) decoder.CompletionFunc {
return c.completionFunctions[hookName]
}
func (m *Modules) Paths() ([]lang.Path, error) {
recs := m.store.ListDirs()
var ret []lang.Path
for _, rec := range recs {
ret = append(ret, lang.Path{
Path: rec,
LanguageID: ourschema.LanguageHCL,
})
}
return ret, nil
}
func (m *Modules) pathContext(p lang.Path) (decoder.Context, error) {
rec := m.store.Get(p.Path)
if rec == nil {
return nil, fmt.Errorf("module not found at path: %s", p.Path)
}
return &pathCtx{
dir: p.Path,
Lookup: ourschema.New(m.provider(p.Path)),
files: rec.Files,
}, nil
}
// dynamicModuleLookup extends DynamicLookup to implement local variable and
// composite schema lookups.
type dynamicModuleLookup struct {
ourschema.DynamicLookup
targetSchema *schema.AttributeSchema
compositeSchema *schema.AttributeSchema
}
func (d *dynamicModuleLookup) LocalSchema(name string) *schema.AttributeSchema {
cons, ok := d.targetSchema.Constraint.(schema.Object)
if !ok {
return nil
}
return cons.Attributes[name]
}
func (d *dynamicModuleLookup) CompositeSchema() *schema.AttributeSchema {
return d.compositeSchema
}
var _ ourschema.LocalsAttributeLookup = &dynamicModuleLookup{}
func (m *Modules) pathCompletionContext(p lang.Path, filename string, pos hcl.Pos) (decoder.CompletionContext, error) {
rec := m.store.Get(p.Path)
if rec == nil {
return nil, fmt.Errorf("module not found at path: %s", p.Path)
}
files := rec.Files
file := files[filename]
if file == nil {
return nil, fmt.Errorf("module file %q not found", filename)
}
block := file.Body.(*hclsyntax.Body).InnermostBlockAtPos(pos)
dyn := m.provider(p.Path)
targets := rec.Targets
if rec.XRD != nil && targets.CompositeSchema == nil {
// we haven't used a composite schema previously; maybe it was still loading
// see if we can redo this correctly.
compositeSchema := dyn.Schema(rec.XRD.APIVersion, rec.XRD.Kind)
if compositeSchema != nil {
targets = target.BuildTargets(rec.Files, dyn, compositeSchema)
rec.Targets = targets
// note that even though the ReferenceMap depends on targets
// it will not change just because we added a schema so no need
// to recompute this.
m.store.Put(rec)
}
}
visibleTargets := targets.VisibleTreeAt(block, filename, pos)
targetSchema := visibleTargets.AsSchema()
return &ctx{
pathCtx: pathCtx{
dir: p.Path,
Lookup: ourschema.New(&dynamicModuleLookup{
DynamicLookup: dyn,
targetSchema: targetSchema,
compositeSchema: targets.CompositeSchema,
}),
files: files,
},
completionFunctions: map[string]decoder.CompletionFunc{
"apiVersion": m.apiVersionCompletion,
"kind": m.kindCompletion,
},
targetSchema: targetSchema,
}, nil
}
func (m *Modules) referenceMap(p lang.Path) (*target.ReferenceMap, error) {
rec := m.store.Get(p.Path)
if rec == nil {
return nil, fmt.Errorf("module not found at path: %s", p.Path)
}
return rec.RefMap, nil
}
package modules
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"github.com/crossplane-contrib/function-hcl/api"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/document"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/eventbus"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/features/modules/store"
lsp "github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langserver/protocol"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/utils/perf"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/utils/queue"
"github.com/ghodss/yaml"
"github.com/hashicorp/hcl/v2"
)
func (m *Modules) start(ctx context.Context) {
m.queue.Start(ctx)
open := m.eventbus.SubscribeToOpenEvents("feature.modules")
edit := m.eventbus.SubscribeToEditEvents("feature.modules")
changeWatch := m.eventbus.SubscribeToChangeWatchEvents("feature.modules")
go func() {
for {
var err error
select {
case event := <-open:
err = m.onOpen(event.Doc.Dir.Path(), event.Doc.Filename)
case event := <-edit:
err = m.onEdit(event.Doc.Dir.Path(), event.Doc.Filename)
case event := <-changeWatch:
err = m.onChangeWatch(event.ChangeType, event.RawPath, event.IsDir)
case <-ctx.Done():
m.logger.Print("stopped modules feature")
return
}
if err != nil {
m.logger.Printf("modules: process event: %q", err)
}
}
}()
}
// ProcessOpenForTesting processes an open event in a completely synchronous fashion
// for use by unit tests in other packages. It assumes that the dir has not been opened.
func (m *Modules) ProcessOpenForTesting(dir string) error {
if m.store.Exists(dir) { // new doc opened for known dir, nothing to do
return fmt.Errorf("module %q already exists", dir)
}
return m.fullParse(dir)()
}
func (m *Modules) onOpen(dir, filename string) (err error) {
if m.store.Exists(dir) {
// Module exists - add the new file to it
content := m.store.Get(dir)
if content == nil {
return nil
}
filePath := filepath.Join(dir, filename)
m.queue.Enqueue(queue.Key(dir), func() error {
return m.incrementalParse(content, filePath)
})
return nil
}
m.queue.Enqueue(queue.Key(dir), m.fullParse(dir))
return nil
}
func (m *Modules) onEdit(dir, filename string) (err error) {
content := m.store.Get(dir)
if content == nil {
return fmt.Errorf("module %q not found when processing edit event", dir)
}
filePath := filepath.Join(dir, filename)
m.queue.Enqueue(queue.Key(dir), func() error {
return m.incrementalParse(content, filePath)
})
return nil
}
func (m *Modules) onChangeWatch(changeType lsp.FileChangeType, rawPath string, isDir bool) error {
if changeType == lsp.Deleted {
path := rawPath
// we don't know whether file or dir is being deleted
// 1st we just blindly try to look it up as a directory
hasModuleRecord := m.store.Exists(path)
if !hasModuleRecord {
// otherwise try the parent dir
path = filepath.Dir(path)
hasModuleRecord = m.store.Exists(path)
}
// nothing to do if not found in our store
if !hasModuleRecord {
return nil
}
// if the path no longer exists, nuke it internally
_, err := os.Stat(path)
if err != nil && os.IsNotExist(err) {
m.queue.Dequeue(queue.Key(path))
m.store.Remove(path)
}
return nil
}
dir := rawPath
filename := ""
if !isDir {
dir = filepath.Dir(rawPath)
filename = filepath.Base(rawPath)
}
// if a file that is being edited is changed, we do not need to reprocess the module.
if filename != "" {
h := document.Handle{
Dir: document.DirHandleFromPath(dir),
Filename: filename,
}
if m.docStore.IsDocumentOpen(h) {
return nil
}
}
x, err := os.Stat(dir)
if err != nil || !x.IsDir() {
m.logger.Printf("error checking existence (%q), or not a directory: %s", dir, err)
return err
}
// If the parent directory exists, we just need to
// check if the there are open documents for the path and that the
// path is a module path. If so, we need to reparse the module.
hasOpenDocs := m.docStore.HasOpenDocuments(document.DirHandleFromPath(dir))
if !hasOpenDocs {
return nil
}
m.queue.Enqueue(queue.Key(dir), m.fullParse(dir))
return nil
}
func (m *Modules) getXRD(dir string) *store.XRD {
b, err := m.fs.ReadFile(filepath.Join(dir, XRDFile))
if err != nil {
return nil
}
var xrd store.XRD
err = yaml.Unmarshal(b, &xrd)
if err != nil {
log.Printf("error parsing XRD file %q: %s", filepath.Join(dir, XRDFile), err)
return nil
}
return &xrd
}
func (m *Modules) analyze(content *store.Content, dir string) {
m.queue.Enqueue(queue.Key(dir+":analysis"), func() error {
var files []api.File
for name, v := range content.Files {
files = append(files, api.File{Name: name, File: v})
}
diags := api.Analyze(files...)
diagsByFile := map[string]hcl.Diagnostics{}
for _, d := range diags {
var rng *hcl.Range
if d.Context != nil {
rng = d.Context
} else {
rng = d.Subject
}
if rng == nil {
continue
}
diagsByFile[rng.Filename] = append(diagsByFile[rng.Filename], d)
}
for filename, fileDiags := range diagsByFile {
m.publishDiagnostics(dir, filename, fileDiags)
}
return nil
})
}
func (m *Modules) incrementalParse(content *store.Content, filePath string) error {
file, diags, err := m.parseModuleFile(filePath)
if err != nil {
return err
}
dir := filepath.Dir(filePath)
filename := filepath.Base(filePath)
content.Files[filename] = file
content.Diags[filename] = diags
dd := m.deriveData(dir, content.Files, content.XRD)
content.Targets = dd.targets
content.RefMap = dd.refMap
m.store.Put(content)
m.publishDiagnostics(dir, filename, diags)
m.analyze(content, dir)
return nil
}
func (m *Modules) fullParse(dir string) func() error {
return func() error {
defer perf.Measure("fullParse")()
files, diags, err := m.loadAndParseModule(dir)
if err != nil {
return err
}
xrd := m.getXRD(dir)
dd := m.deriveData(dir, files, xrd)
content := store.Content{
Path: dir,
Files: files,
Diags: diags,
Targets: dd.targets,
RefMap: dd.refMap,
XRD: xrd,
}
m.store.Put(&content)
for filename, fileDiags := range diags {
m.publishDiagnostics(dir, filename, fileDiags)
}
m.analyze(&content, dir)
return nil
}
}
func (m *Modules) publishDiagnostics(dir, filename string, diags hcl.Diagnostics) {
m.eventbus.PublishDiagnosticsEvent(eventbus.DiagnosticsEvent{
Doc: document.Handle{
Dir: document.DirHandleFromPath(dir),
Filename: filename,
},
Diags: diags,
})
}
package modules
import (
"fmt"
"sort"
"strings"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/funchcl/decoder"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/lang"
"github.com/zclconf/go-cty/cty"
)
type fieldType int
const (
typeAPIVersion fieldType = iota
typeKind
)
func (m *Modules) makeCandidates(ctx decoder.CompletionFuncContext, t fieldType, prefix string) []lang.HookCandidate {
filterValue, err := m.findOtherValueFor(ctx, t)
if err != nil {
m.logger.Println(err) // and continue
}
dyn := m.provider(ctx.Dir)
seen := map[string]bool{}
for _, ak := range dyn.Keys() {
switch t {
case typeAPIVersion:
if filterValue != "" && ak.Kind != filterValue {
continue
}
if !strings.HasPrefix(ak.ApiVersion, prefix) {
continue
}
seen[ak.ApiVersion] = true
default:
if filterValue != "" && ak.ApiVersion != filterValue {
continue
}
if !strings.HasPrefix(ak.Kind, prefix) {
continue
}
seen[ak.Kind] = true
}
}
ret := make([]lang.HookCandidate, 0, len(seen))
keys := make([]string, 0, len(seen))
for k := range seen {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
// log.Println("candidate:", k)
ret = append(ret, lang.ExpressionCompletionCandidate(lang.ExpressionCandidate{
Value: cty.StringVal(k),
}))
}
return ret
}
func (m *Modules) findOtherValueFor(ctx decoder.CompletionFuncContext, t fieldType) (string, error) {
body, ok := ctx.PathContext.HCLFileByName(ctx.Filename)
if !ok {
return "", fmt.Errorf("find position: no body for file %s", ctx.Filename)
}
bodyAttr := body.AttributeAtPos(ctx.Pos)
if bodyAttr == nil {
return "", fmt.Errorf("find position: nil body attr at pos %v", ctx.Pos)
}
fld := "apiVersion"
if t == typeAPIVersion {
fld = "kind"
}
v, _ := bodyAttr.Expr.Value(nil)
if v.IsNull() || !v.IsKnown() {
return "", fmt.Errorf("find position: incomplete value for %s", fld)
}
if !v.Type().IsObjectType() {
return "", fmt.Errorf("find position: expected object attribute, found %v", v.Type())
}
obj := v.AsValueMap()
v2 := obj[fld]
if v2.Type() != cty.String {
return "", fmt.Errorf("find position: expected string attribute for %s, found %v", fld, v2.Type())
}
return v2.AsString(), nil
}
func (m *Modules) apiVersionCompletion(ctx decoder.CompletionFuncContext, matchPrefix string) ([]lang.HookCandidate, error) {
return m.makeCandidates(ctx, typeAPIVersion, matchPrefix), nil
}
func (m *Modules) kindCompletion(ctx decoder.CompletionFuncContext, matchPrefix string) ([]lang.HookCandidate, error) {
return m.makeCandidates(ctx, typeKind, matchPrefix), nil
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package modules
import (
"path/filepath"
"strings"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/features/modules/store"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/funchcl/target"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/schema"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/utils/perf"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclparse"
)
func isModuleFilename(name string) bool {
return strings.HasSuffix(name, ".hcl")
}
func (m *Modules) loadAndParseModule(modPath string) (map[string]*hcl.File, map[string]hcl.Diagnostics, error) {
fs := m.fs
parser := hclparse.NewParser()
files := map[string]*hcl.File{}
diags := map[string]hcl.Diagnostics{}
infos, err := fs.ReadDir(modPath)
if err != nil {
return nil, nil, err
}
for _, info := range infos {
if info.IsDir() {
continue
}
name := info.Name()
if !isModuleFilename(name) {
continue
}
fullPath := filepath.Join(modPath, name)
src, err := fs.ReadFile(fullPath)
if err != nil {
m.logger.Printf("error reading file: %v", err)
// If a file isn't accessible, continue with reading the
// remaining module files
continue
}
f, pDiags := parser.ParseHCL(src, name)
diags[name] = pDiags
if f != nil {
files[name] = f
}
}
return files, diags, nil
}
func (m *Modules) parseModuleFile(filePath string) (*hcl.File, hcl.Diagnostics, error) {
fs := m.fs
parser := hclparse.NewParser()
src, err := fs.ReadFile(filePath)
if err != nil {
return nil, nil, err
}
f, pDiags := parser.ParseHCL(src, filepath.Base(filePath))
return f, pDiags, nil
}
type derivedData struct {
targets *target.Targets
refMap *target.ReferenceMap
}
func (m *Modules) deriveData(modPath string, files map[string]*hcl.File, xrd *store.XRD) derivedData {
defer perf.Measure("derivedData")()
lookup := m.provider(modPath)
var compositeSchema *schema.AttributeSchema
if xrd != nil {
compositeSchema = lookup.Schema(xrd.APIVersion, xrd.Kind)
}
targets := target.BuildTargets(files, m.provider(modPath), compositeSchema)
refMap := target.BuildReferenceMap(files, targets)
return derivedData{targets: targets, refMap: refMap}
}
// Package store tracks derived information for modules. A module is equivalent to a directory on the filesystem.
package store
import (
"sync"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/funchcl/target"
"github.com/hashicorp/hcl/v2"
)
func copyMap[T any](in map[string]T) map[string]T {
ret := map[string]T{}
for k, v := range in {
ret[k] = v
}
return ret
}
// XRD captures the API version and kind of the composite associated with the module.
type XRD struct {
APIVersion string `json:"apiVersion"`
Kind string `json:"kind"`
}
// Content contains information being tracked for a module.
type Content struct {
Path string // the directory of the module.
Files map[string]*hcl.File // parsed files by unqualified filename.
Diags map[string]hcl.Diagnostics // diags by unqualified filename.
Targets *target.Targets // computed reference targets.
RefMap *target.ReferenceMap // computed reference map.
XRD *XRD // XRD information, if present in the module metadata file.
}
// toModule adapts a content record to module format.
func (c *Content) toModule() *module {
return &module{
path: c.Path,
files: copyMap(c.Files),
diags: copyMap(c.Diags),
refMap: c.RefMap,
targets: c.Targets,
xrd: c.XRD,
}
}
// module represents the information we need to track for every module that is processed.
type module struct {
path string
files map[string]*hcl.File
diags map[string]hcl.Diagnostics
targets *target.Targets
refMap *target.ReferenceMap
xrd *XRD
}
// Content returns the current contents of the module as a read only copy.
func (m *module) Content() *Content {
return &Content{
Path: m.path,
Files: copyMap(m.files),
Diags: copyMap(m.diags),
Targets: m.targets,
RefMap: m.refMap,
XRD: m.xrd,
}
}
// Store tracks module state for multiple directories.
type Store struct {
l sync.RWMutex
modules map[string]*module
}
// New creates a new store.
func New() *Store {
return &Store{
modules: map[string]*module{},
}
}
// ListDirs returns all known module directories.
func (s *Store) ListDirs() []string {
s.l.RLock()
defer s.l.RUnlock()
var ret []string
for k := range s.modules {
ret = append(ret, k)
}
return ret
}
// Exists returns true if the module store is currently tracking the supplied directory.
func (s *Store) Exists(dir string) bool {
s.l.RLock()
defer s.l.RUnlock()
return s.modules[dir] != nil
}
// Remove removes stored information for the supplied directory if such information exists.
func (s *Store) Remove(dir string) {
s.l.Lock()
defer s.l.Unlock()
delete(s.modules, dir)
}
// Get returns module information for the supplied directory or nil if no such information exists.
func (s *Store) Get(dir string) *Content {
s.l.RLock()
defer s.l.RUnlock()
m := s.modules[dir]
if m == nil {
return nil
}
return m.Content()
}
// Put adds or updates information stored for the supplied module.
func (s *Store) Put(content *Content) {
s.l.Lock()
defer s.l.Unlock()
s.modules[content.Path] = content.toModule()
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package filesystem
import (
"bytes"
"io/fs"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/document"
)
func documentAsFile(doc *document.Document) fs.File {
return &inMemFile{
reader: bytes.NewReader(doc.Text),
info: documentAsFileInfo(doc),
}
}
func documentAsFileInfo(doc *document.Document) fs.FileInfo {
return inMemFileInfo{
name: doc.Filename,
size: len(doc.Text),
modTime: doc.ModTime,
mode: 0o644,
isDir: false,
}
}
func documentsAsDirEntries(docs []*document.Document) []fs.DirEntry {
entries := make([]fs.DirEntry, len(docs))
for i, doc := range docs {
entries[i] = documentAsDirEntry(doc)
}
return entries
}
func documentAsDirEntry(doc *document.Document) fs.DirEntry {
return inMemDirEntry{
name: doc.Filename,
isDir: false,
typ: 0,
info: documentAsFileInfo(doc),
}
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
// Package filesystem provides an FS abstraction over operating system files overlaid
// with unsaved editor content.
package filesystem
import (
"fmt"
"io/fs"
"log"
"os"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/document"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/utils/logging"
)
// Filesystem provides io/fs.FS compatible two-layer read-only filesystem
// with preferred source being DocumentStore and native OS FS acting as fallback.
//
// This allows for reading files in a directory while reflecting unsaved changes.
type Filesystem struct {
osFs osFs
docStore DocumentStore
logger *log.Logger
}
// DocumentStore proves list and get facilities for unsaved documents.
type DocumentStore interface {
Get(document.Handle) (*document.Document, error)
List(document.DirHandle) []*document.Document
}
// New creates an OS filesystem overlaid with the supplied document store.
func New(docStore DocumentStore) *Filesystem {
return &Filesystem{
osFs: osFs{},
docStore: docStore,
logger: logging.LoggerFor(logging.ModuleFilesystem),
}
}
// ReadFile provides the content at the supplied path.
func (f *Filesystem) ReadFile(name string) ([]byte, error) {
doc, err := f.docStore.Get(document.HandleFromPath(name))
if err != nil {
if document.IsNotFound(err) {
return f.osFs.ReadFile(name)
}
return nil, err
}
return doc.Text, err
}
// ReadDir provides entries under the supplied directory path.
func (f *Filesystem) ReadDir(dir string) ([]fs.DirEntry, error) {
dirHandle := document.DirHandleFromPath(dir)
docList := f.docStore.List(dirHandle)
osList, err := f.osFs.ReadDir(dir)
if err != nil && !os.IsNotExist(err) {
return nil, fmt.Errorf("OS FS: %w", err)
}
list := documentsAsDirEntries(docList)
for _, osEntry := range osList {
if entryIsInList(list, osEntry) {
continue
}
list = append(list, osEntry)
}
return list, nil
}
func entryIsInList(list []fs.DirEntry, entry fs.DirEntry) bool {
for _, di := range list {
if di.Name() == entry.Name() {
return true
}
}
return false
}
// Open implements fs.FS.
func (f *Filesystem) Open(name string) (fs.File, error) {
doc, err := f.docStore.Get(document.HandleFromPath(name))
if err != nil {
if document.IsNotFound(err) {
return f.osFs.Open(name)
}
return nil, err
}
return documentAsFile(doc), err
}
// Stat provides file information at the supplied path.
func (f *Filesystem) Stat(name string) (os.FileInfo, error) {
doc, err := f.docStore.Get(document.HandleFromPath(name))
if err != nil {
if document.IsNotFound(err) {
return f.osFs.Stat(name)
}
return nil, err
}
return documentAsFileInfo(doc), err
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package filesystem
import (
"bytes"
"io/fs"
"time"
)
type inMemFile struct {
info fs.FileInfo
reader *bytes.Reader
}
func (f *inMemFile) Read(b []byte) (int, error) {
return f.reader.Read(b)
}
func (f *inMemFile) Stat() (fs.FileInfo, error) {
return f.info, nil
}
func (f *inMemFile) Close() error {
return nil
}
type inMemFileInfo struct {
name string
size int
mode fs.FileMode
modTime time.Time
isDir bool
}
func (fi inMemFileInfo) Name() string {
return fi.name
}
func (fi inMemFileInfo) Size() int64 {
return int64(fi.size)
}
func (fi inMemFileInfo) Mode() fs.FileMode {
return fi.mode
}
func (fi inMemFileInfo) ModTime() time.Time {
return fi.modTime
}
func (fi inMemFileInfo) IsDir() bool {
return fi.isDir
}
func (fi inMemFileInfo) Sys() interface{} {
return nil
}
type inMemDirEntry struct {
name string
isDir bool
typ fs.FileMode
info fs.FileInfo
}
func (de inMemDirEntry) Name() string {
return de.name
}
func (de inMemDirEntry) IsDir() bool {
return de.isDir
}
func (de inMemDirEntry) Type() fs.FileMode {
return de.typ
}
func (de inMemDirEntry) Info() (fs.FileInfo, error) {
return de.info, nil
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package filesystem
import (
"io/fs"
"os"
)
type osFs struct{}
func (o osFs) Open(name string) (fs.File, error) {
return os.Open(name)
}
func (o osFs) Stat(name string) (fs.FileInfo, error) {
return os.Stat(name)
}
func (o osFs) ReadDir(name string) ([]fs.DirEntry, error) {
return os.ReadDir(name)
}
func (o osFs) ReadFile(name string) ([]byte, error) {
return os.ReadFile(name)
}
package decoder
// LangServerBehavior contains flags that influence how the language server behaves
// based on calling client identity. A singleton instance is initialized during the
// LSP initialize call and made accessible to feature implementations via Context.
type LangServerBehavior struct {
MaxCompletionItems int // max completion items to return (0 means use default of 100)
IndentMultiLineProposals bool // when true, add leading spaces to multiple proposals based on current indent
InnerBraceRangesForFolding bool // when true, ensure that folding range is the range not including braces
}
var defaultBehavior LangServerBehavior
// SetBehavior sets the singleton LangServerBehavior instance.
func SetBehavior(b LangServerBehavior) {
defaultBehavior = b
}
// GetBehavior returns the singleton LangServerBehavior instance.
func GetBehavior() LangServerBehavior {
return defaultBehavior
}
// Package completion provides facilities for auto-complete and hover information.
package completion
import (
"log"
"os"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/funchcl/decoder"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/lang"
"github.com/hashicorp/hcl/v2"
)
// these variables control internal expression debugging but are only enabled in
// code to minimize perf impact, such that the compiler can optimize away all
// debug code paths when the variable is false.
var (
debugCompletion = false
dumpDebugSource = false
debugLogger = log.New(os.Stderr, "", 0)
)
// Completer provides completion and hover information.
type Completer struct {
ctx decoder.CompletionContext
maxCandidates int
}
// New creates a Completer.
func New(ctx decoder.CompletionContext) *Completer {
maxCandidates := 100
if n := decoder.GetBehavior().MaxCompletionItems; n > 0 {
maxCandidates = n
}
return &Completer{
ctx: ctx,
maxCandidates: maxCandidates,
}
}
// CompletionAt returns completion candidates for a given position in a file.
func (c *Completer) CompletionAt(filename string, pos hcl.Pos) (ret lang.Candidates, _ error) {
list, err := c.startCompletion(filename, pos)
if err != nil {
return ret, err
}
complete := true
if len(list) > c.maxCandidates {
list = list[:c.maxCandidates]
complete = false
}
return lang.Candidates{
List: list,
IsComplete: complete,
}, nil
}
// HoverAt returns hover data for a given position in a file.
func (c *Completer) HoverAt(filename string, pos hcl.Pos) (*lang.HoverData, error) {
return c.doHover(filename, pos)
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package completion
import (
"fmt"
"sort"
"strings"
ourschema "github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/funchcl/schema"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/lang"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/schema"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/writer"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
)
func (c *Completer) startCompletion(filename string, pos hcl.Pos) ([]lang.Candidate, error) {
var candidates []lang.Candidate
f, err := c.fileByName(filename)
if err != nil {
return candidates, err
}
rootBody, err := c.bodyForFileAndPos(filename, f, pos)
if err != nil {
return candidates, err
}
if dumpDebugSource {
debugLogger.Printf("using source:\n%s\n", writer.NodeToSource(rootBody))
}
return c.completeBodyAtPos(rootBody, schema.NewBlockStack(), pos)
}
func (c *Completer) completeBodyAtPos(body *hclsyntax.Body, bs schema.BlockStack, pos hcl.Pos) ([]lang.Candidate, error) {
var candidates []lang.Candidate
filename := body.Range().Filename
declared := make(declaredAttributes, len(body.Attributes))
for _, attr := range body.Attributes {
declared[attr.Name] = attr.Range()
}
// process position inside an attribute
for _, attr := range body.Attributes {
if c.isPosInsideAttrExpr(attr, pos) {
aSchema := c.ctx.AttributeSchema(bs, attr.Name)
// special-case: for resource bodies having an object schema, remove the status attribute
parentBlock := bs.Peek(0).Type
if attr.Name == "body" && (parentBlock == "resource" || parentBlock == "template") {
aSchema = ourschema.WithoutStatus(aSchema)
}
return c.attrValueCompletionAtPos(attr, aSchema, pos)
}
if attr.NameRange.ContainsPos(pos) || posEqual(attr.NameRange.End, pos) {
prefixRng := attr.NameRange
prefixRng.End = pos
return c.bodySchemaCandidates(c.ctx.BodySchema(bs), prefixRng, attr.Range(), declared), nil
}
if attr.EqualsRange.ContainsPos(pos) {
return candidates, nil
}
}
rng := hcl.Range{
Filename: filename,
Start: pos,
End: pos,
}
processBlock := func(block *hclsyntax.Block) ([]lang.Candidate, error) {
parentSchema := c.ctx.BodySchema(bs)
bs.Push(block)
childSchema := c.ctx.BodySchema(bs)
if childSchema == nil {
return candidates, nil
}
if block.TypeRange.ContainsPos(pos) {
prefixRng := block.TypeRange
prefixRng.End = pos
return c.bodySchemaCandidates(parentSchema, prefixRng, block.Range(), declared), nil
}
for _, labelRange := range block.LabelRanges {
if labelRange.ContainsPos(pos) || posEqual(labelRange.End, pos) {
return candidates, nil // we've already sent allowed values for labels, nothing more to see here
}
}
if isPosOutsideBody(block, pos) {
return candidates, &positionalError{
filename: filename,
pos: pos,
msg: fmt.Sprintf("position outside of %q body", block.Type),
}
}
if block.Body != nil && block.Body.Range().ContainsPos(pos) {
return c.completeBodyAtPos(block.Body, bs, pos)
}
return nil, nil
}
// process position inside blocks
for _, block := range body.Blocks {
if !block.Range().ContainsPos(pos) {
continue
}
return processBlock(block)
}
tokenRng, err := c.nameTokenRangeAtPos(body.Range().Filename, pos)
if err == nil {
rng = tokenRng
}
return c.bodySchemaCandidates(c.ctx.BodySchema(bs), rng, rng, declared), nil
}
func (c *Completer) blockSchemaToCandidate(blockType string, block *schema.BasicBlockSchema, rng hcl.Range) lang.Candidate {
triggerSuggest := false
if len(block.Labels) > 0 {
triggerSuggest = block.Labels[0].CanComplete()
}
return lang.Candidate{
Label: blockType,
Detail: detailForBlock(block),
Description: block.Description,
Kind: lang.BlockCandidateKind,
TextEdit: lang.TextEdit{
NewText: blockType,
Snippet: snippetForBlock(blockType, block),
Range: rng,
},
TriggerSuggest: triggerSuggest,
}
}
// bodySchemaCandidates returns candidates for completion of fields inside a body or block.
func (c *Completer) bodySchemaCandidates(schema *schema.BodySchema, prefixRng, editRng hcl.Range, declared declaredAttributes) []lang.Candidate {
prefix, _ := c.bytesFromRange(prefixRng)
pfx := string(prefix)
candidates := attributeCandidates(pfx, schema.Attributes, declared, editRng)
for name, block := range schema.NestedBlocks {
if len(pfx) > 0 && !strings.HasPrefix(name, pfx) {
continue
}
candidates = append(candidates, c.blockSchemaToCandidate(name, block, editRng))
}
sort.Slice(candidates, func(i, j int) bool {
return candidates[i].Label < candidates[j].Label
})
return candidates
}
func (c *Completer) isPosInsideAttrExpr(attr *hclsyntax.Attribute, pos hcl.Pos) bool {
if attr.Expr.Range().ContainsPos(pos) {
return true
}
// edge case: near end (typically newline char)
if attr.Expr.Range().End.Byte == pos.Byte {
return true
}
// edge case: near the beginning (right after '=')
if attr.EqualsRange.End.Byte == pos.Byte {
return true
}
// edge case: end of incomplete expression with trailing '.' (which parser ignores)
endByte := attr.Expr.Range().End.Byte
if pos.Byte-endByte == 1 {
suspectedDotRng := hcl.Range{
Filename: attr.Expr.Range().Filename,
Start: attr.Expr.Range().End,
End: pos,
}
b, err := c.bytesFromRange(suspectedDotRng)
if err == nil && string(b) == "." {
return true
}
}
return false
}
func (c *Completer) attrValueCompletionAtPos(attr *hclsyntax.Attribute, s *schema.AttributeSchema, pos hcl.Pos) ([]lang.Candidate, error) {
if len(s.CompletionHooks) > 0 {
return candidatesFromHooks(c.ctx, attr.Expr, s, pos), nil
}
ec := newExpressionCompleter(c.ctx, pos)
return ec.complete(attr.Expr, s), nil
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package completion
import (
"fmt"
"github.com/hashicorp/hcl/v2"
)
type fileNotFoundError struct {
filename string
}
func (e *fileNotFoundError) Error() string {
return fmt.Sprintf("%s: file not found", e.filename)
}
type posOutOfRangeError struct {
filename string
pos hcl.Pos
rng hcl.Range
}
func (e *posOutOfRangeError) Error() string {
return fmt.Sprintf("%s: position %s is out of range %s", e.filename, posToStr(e.pos), e.rng)
}
type positionalError struct {
filename string
pos hcl.Pos
msg string
}
func (e *positionalError) Error() string {
return fmt.Sprintf("%s (%s): %s", e.filename, posToStr(e.pos), e.msg)
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package completion
import (
"bytes"
"fmt"
"sort"
"strings"
"unicode/utf8"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/lang"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/schema"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
)
func (e *expressionCompleter) completeFunction(expr hclsyntax.Expression, as *schema.AttributeSchema) []lang.Candidate {
pos := e.pos
if isEmptyExpression(expr) {
editRange := hcl.Range{
Filename: expr.Range().Filename,
Start: pos,
End: pos,
}
return e.matchingFunctions("", editRange, as)
}
switch eType := expr.(type) {
case *hclsyntax.ScopeTraversalExpr:
if len(eType.Traversal) > 1 {
// we assume that function names cannot contain dots
return nil
}
prefixLen := pos.Byte - eType.Traversal.SourceRange().Start.Byte
rootName := eType.Traversal.RootName()
// There can be a single segment with trailing dot which cannot
// be a function anymore as functions cannot contain dots.
if prefixLen < 0 || prefixLen > len(rootName) {
return nil
}
prefix := rootName[0:prefixLen]
return e.matchingFunctions(prefix, eType.Range(), as)
case *hclsyntax.ExprSyntaxError:
// Note: this range can range up until the end of the file in case of invalid config.
// The HCL parser does support the :: namespace syntax for function names (since
// hashicorp/hcl#639), but it can still produce ExprSyntaxError when the user is
// in the middle of typing a namespaced function name and the expression is not yet
// syntactically complete (e.g. "provider::" with nothing after it, or missing parens).
if eType.SrcRange.ContainsPos(pos) {
// recover bytes around the cursor to check whether the user is partially
// typing a namespaced function name
fileBytes := e.ctx.FileBytes(eType)
recoveredPrefixBytes := recoverLeftBytes(fileBytes, pos, func(offset int, r rune) bool {
return !isNamespacedFunctionNameRune(r)
})
// recoveredPrefixBytes also contains the rune before the function name, so we need to trim it
_, lengthFirstRune := utf8.DecodeRune(recoveredPrefixBytes)
recoveredPrefixBytes = recoveredPrefixBytes[lengthFirstRune:]
recoveredSuffixBytes := recoverRightBytes(fileBytes, pos, func(offset int, r rune) bool {
return !isNamespacedFunctionNameRune(r) && r != '('
})
// recoveredSuffixBytes also contains the rune after the function name, so we need to trim it
_, lengthLastRune := utf8.DecodeLastRune(recoveredSuffixBytes)
recoveredSuffixBytes = recoveredSuffixBytes[:len(recoveredSuffixBytes)-lengthLastRune]
recoveredIdentifier := append(recoveredPrefixBytes, recoveredSuffixBytes...)
// check if our recovered identifier contains "::"
// Why two colons? For no colons the parser would return a traversal expression
// and a single colon will apparently be treated as a traversal and a partial object expression
// (refer to this follow-up issue for more on that case: https://github.com/hashicorp/vscode-terraform/issues/1697)
if bytes.Contains(recoveredIdentifier, []byte("::")) {
editRange := hcl.Range{
Filename: expr.Range().Filename,
Start: hcl.Pos{
Line: pos.Line, // we don't recover newlines, so we can keep the original line number
Byte: pos.Byte - len(recoveredPrefixBytes),
Column: pos.Column - len(recoveredPrefixBytes),
},
End: hcl.Pos{
Line: pos.Line,
Byte: pos.Byte + len(recoveredSuffixBytes),
Column: pos.Column + len(recoveredSuffixBytes),
},
}
return e.matchingFunctions(string(recoveredPrefixBytes), editRange, as)
}
}
return nil
}
return nil
}
func (e *expressionCompleter) matchingFunctions(prefix string, editRange hcl.Range, as *schema.AttributeSchema) []lang.Candidate {
var candidates []lang.Candidate
// DODGY: we are completing literal true and false here instead of in a sane place :(
if _, ok := as.Constraint.(schema.Bool); ok {
candidates = boolLiteralTypeCandidates(prefix, editRange)
}
for name, f := range e.ctx.Functions() {
/*
the terraform language server makes special checks to ensure that the return type of the function
matches the LHS. However, consider:
foo_boolean = cidrhost(..) == "something"
In this case, even though the cidrhost function doesn't return a boolean, it can still be used as part of an
expression for a boolean LHS. In fact, literally any function can be used given conditional expressions,
logical comparisons etc. So we don't bother to check the return type and send _all_ functions that match
the prefix.
*/
if !strings.HasPrefix(name, prefix) {
continue
}
candidates = append(candidates, lang.Candidate{
Label: name,
Detail: fmt.Sprintf("%s(%s) %s", name, parameterNamesAsString(f), f.ReturnType.FriendlyName()),
Kind: lang.FunctionCandidateKind,
Description: lang.Markdown(f.Description),
TextEdit: lang.TextEdit{
NewText: fmt.Sprintf("%s()", name),
Snippet: fmt.Sprintf("%s(${0})", name),
Range: editRange,
},
})
}
sort.SliceStable(candidates, func(i, j int) bool {
return candidates[i].Label < candidates[j].Label
})
return candidates
}
package completion
import (
"bytes"
"sort"
"strings"
"unicode"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/lang"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/schema"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
)
type declaredAttributes map[string]hcl.Range
func (e *expressionCompleter) completeObject(expr *hclsyntax.ObjectConsExpr, obj schema.Object) []lang.Candidate {
pos := e.pos
betweenBraces := hcl.Range{
Filename: expr.Range().Filename,
Start: expr.OpenRange.End,
End: expr.Range().End,
}
if !betweenBraces.ContainsPos(pos) {
return nil
}
editRange := hcl.Range{
Filename: expr.Range().Filename,
Start: pos,
End: pos,
}
declared := declaredAttributes{}
recoveryPos := expr.OpenRange.Start
var lastItemRange, nextItemRange *hcl.Range
for _, item := range expr.Items {
emptyRange := hcl.Range{
Filename: expr.Range().Filename,
Start: item.KeyExpr.Range().End,
End: item.ValueExpr.Range().Start,
}
if emptyRange.ContainsPos(pos) {
// exit early if we're in empty space between key and value
return nil
}
attrName, attrRange, isRawName := rawObjectKey(item.KeyExpr)
if isRawName {
// collect all declared attributes
declared[attrName] = hcl.RangeBetween(item.KeyExpr.Range(), item.ValueExpr.Range())
}
if nextItemRange != nil {
continue
}
// check if we've just missed the position
if pos.Byte < item.KeyExpr.Range().Start.Byte {
// record current (next) item so we can avoid completion
// on the same line in multi-line mode (without comma)
nextItemRange = hcl.RangeBetween(item.KeyExpr.Range(), item.ValueExpr.Range()).Ptr()
// enable recovery of incomplete configuration
// between last item's end and position
continue
}
lastItemRange = hcl.RangeBetween(item.KeyExpr.Range(), item.ValueExpr.Range()).Ptr()
recoveryPos = item.ValueExpr.Range().End
if item.KeyExpr.Range().ContainsPos(pos) {
// handle any interpolation if it is allowed
keyExpr, ok := item.KeyExpr.(*hclsyntax.ObjectConsKeyExpr)
if ok && obj.AllowInterpolatedKeys {
parensExpr, ok := keyExpr.Wrapped.(*hclsyntax.ParenthesesExpr)
if ok {
return e.complete(parensExpr, &schema.AttributeSchema{Constraint: schema.String{}})
}
}
if isRawName {
prefix := ""
// if we're before start of the attribute
// it means the attribute is likely quoted
if pos.Byte >= attrRange.Start.Byte {
prefixLen := pos.Byte - attrRange.Start.Byte
prefix = attrName[0:prefixLen]
}
editRange = hcl.RangeBetween(item.KeyExpr.Range(), item.ValueExpr.Range())
return attributeCandidates(prefix, obj.Attributes, declared, editRange)
}
return nil
}
if e.inCompletionRange(item.ValueExpr) {
aSchema, ok := obj.Attributes[attrName]
if !ok {
if obj.AnyAttribute != nil {
aSchema = &schema.AttributeSchema{Constraint: obj.AnyAttribute}
} else {
aSchema = &schema.AttributeSchema{Constraint: schema.Any{}}
}
}
return e.complete(item.ValueExpr, aSchema)
}
}
// check any incomplete configuration up to a terminating character
fileBytes := e.ctx.FileBytes(expr)
leftBytes := recoverLeftBytes(fileBytes, pos, func(offset int, r rune) bool {
return isObjectItemTerminatingRune(r) && offset > recoveryPos.Byte
})
trimmedBytes := bytes.TrimRight(leftBytes, " \t")
if len(trimmedBytes) == 0 {
// no terminating character was found which indicates
// we're on the same line as an existing item,
// and we're missing preceding comma
return nil
}
if len(trimmedBytes) == 1 && isObjectItemTerminatingRune(rune(trimmedBytes[0])) {
// avoid completing on the same line as next item
if nextItemRange != nil && nextItemRange.Start.Line == pos.Line {
return nil
}
// avoid completing on the same line as last item
if lastItemRange != nil && lastItemRange.End.Line == pos.Line {
// if it is not single-line notation
if trimmedBytes[0] != ',' {
return nil
}
}
return attributeCandidates("", obj.Attributes, declared, editRange)
}
// trim left side as well now
// to make prefix/attribute extraction easier below
trimmedBytes = bytes.TrimLeftFunc(trimmedBytes, func(r rune) bool {
return isObjectItemTerminatingRune(r) || unicode.IsSpace(r)
})
// parenthesis implies interpolated attribute name
if trimmedBytes[len(trimmedBytes)-1] == '(' && obj.AllowInterpolatedKeys {
emptyExpr := newEmptyExpressionAtPos(expr.Range().Filename, pos)
return e.complete(emptyExpr.(hclsyntax.Expression), &schema.AttributeSchema{Constraint: schema.String{}})
}
// if last byte is =, then it's incomplete attribute
//nolint:staticcheck
if len(trimmedBytes) > 0 && trimmedBytes[len(trimmedBytes)-1] == '=' {
emptyExpr := newEmptyExpressionAtPos(expr.Range().Filename, pos)
attrName := string(bytes.TrimFunc(trimmedBytes[:len(trimmedBytes)-1], func(r rune) bool {
return unicode.IsSpace(r) || r == '"'
}))
aSchema, ok := obj.Attributes[attrName]
if !ok {
if obj.AnyAttribute != nil {
aSchema = &schema.AttributeSchema{Constraint: obj.AnyAttribute}
} else {
// unknown attribute
return nil
}
}
return e.complete(emptyExpr.(hclsyntax.Expression), aSchema)
}
prefix := string(bytes.TrimFunc(trimmedBytes, func(r rune) bool {
return unicode.IsSpace(r) || r == '"'
}))
// calculate appropriate edit range in case there
// are also characters on the right from position
// which are worth replacing
remainingRange := hcl.Range{
Filename: expr.Range().Filename,
Start: pos,
End: expr.SrcRange.End,
}
editRange = e.objectItemPrefixBasedEditRange(remainingRange, fileBytes, trimmedBytes)
return attributeCandidates(prefix, obj.Attributes, declared, editRange)
}
func (e *expressionCompleter) objectItemPrefixBasedEditRange(remainingRange hcl.Range, fileBytes []byte, rawPrefixBytes []byte) hcl.Range {
remainingBytes := remainingRange.SliceBytes(fileBytes)
roughEndByteOffset := bytes.IndexFunc(remainingBytes, func(r rune) bool {
return r == '\n' || r == '}'
})
if roughEndByteOffset >= 0 {
remainingBytes = remainingBytes[roughEndByteOffset:]
}
// avoid editing over whitespace
trimmedRightBytes := bytes.TrimRightFunc(remainingBytes, func(r rune) bool {
return unicode.IsSpace(r)
})
trimmedOffset := len(trimmedRightBytes)
return hcl.Range{
Filename: remainingRange.Filename,
Start: hcl.Pos{
// TODO: Calculate Line+Column for multi-line keys?
Line: remainingRange.Start.Line,
Column: remainingRange.Start.Column - len(rawPrefixBytes),
Byte: remainingRange.Start.Byte - len(rawPrefixBytes),
},
End: hcl.Pos{
// TODO: Calculate Line+Column for multi-line values?
Line: remainingRange.Start.Line,
Column: remainingRange.Start.Column + trimmedOffset,
Byte: remainingRange.Start.Byte + trimmedOffset,
},
}
}
// attributeCandidates returns completion candidates for attributes matching prefix.
// Already-declared attributes are excluded unless their range overlaps editRange.
func attributeCandidates(prefix string, attrs map[string]*schema.AttributeSchema, declared declaredAttributes, editRange hcl.Range) []lang.Candidate {
if len(attrs) == 0 {
return nil
}
names := make([]string, 0, len(attrs))
for n := range attrs {
names = append(names, n)
}
sort.Strings(names)
var candidates []lang.Candidate
for _, name := range names {
if len(prefix) > 0 && !strings.HasPrefix(name, prefix) {
continue
}
if declared != nil {
if declaredRng, ok := declared[name]; ok && !declaredRng.Overlaps(editRange) {
continue
}
}
candidates = append(candidates, attributeSchemaToCandidate(name, attrs[name], editRange))
}
return candidates
}
package completion
import (
"log"
"strings"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/lang"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/schema"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
)
func (e *expressionCompleter) findRefMatches(cons schema.Constraint, pathElements []string, editRange hcl.Range) []lang.Candidate {
objCons, ok := cons.(schema.Object)
if !ok {
return nil
}
if len(pathElements) == 0 {
log.Println("internal error: no path elements")
return nil
}
first := pathElements[0]
rest := pathElements[1:]
remaining := len(rest)
var ret []lang.Candidate
switch remaining {
case 0:
for name, attr := range objCons.Attributes {
if strings.HasPrefix(name, first) {
text := name
ret = append(ret, lang.Candidate{
Label: name,
Description: attr.Description,
Kind: lang.ReferenceCandidateKind,
TextEdit: lang.TextEdit{
NewText: text,
Snippet: text,
Range: editRange,
},
})
}
}
return ret
default:
for name, attr := range objCons.Attributes {
if name == first {
ret = e.findRefMatches(attr.Constraint, rest, editRange)
}
}
}
return ret
}
func (e *expressionCompleter) completeRef(expr hclsyntax.Expression, _ *schema.AttributeSchema) []lang.Candidate {
var candidates []lang.Candidate
var prefixRange hcl.Range
ctx := e.ctx
pos := e.pos
if isEmptyExpression(expr) {
prefixRange = hcl.Range{
Filename: expr.Range().Filename,
Start: pos,
End: pos,
}
}
switch expr := expr.(type) {
case *hclsyntax.ScopeTraversalExpr, *hclsyntax.ExprSyntaxError:
prefixRange = expr.Range()
prefixRange.End = pos
}
if prefixRange.Filename == "" {
return candidates
}
prefix := string(prefixRange.SliceBytes(ctx.FileBytes(expr)))
if strings.Contains(prefix, "[") || strings.Contains(prefix, "*") { // we don't yet support anything other than a.b.c.d for now
return candidates
}
elements := strings.Split(prefix, ".")
editRange := prefixRange
lastDot := strings.LastIndex(prefix, ".")
if lastDot >= 0 {
editRange.Start.Column += lastDot + 1
editRange.Start.Byte += lastDot + 1
}
return e.findRefMatches(ctx.TargetSchema().Constraint, elements, editRange)
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package completion
import (
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/lang"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/schema"
"github.com/hashicorp/hcl/v2/hclsyntax"
)
func (e *expressionCompleter) completeTemplateExpr(eType *hclsyntax.TemplateExpr) []lang.Candidate {
if eType.IsStringLiteral() {
return nil
}
pos := e.pos
for _, partExpr := range eType.Parts {
// We overshot the position and stop
if partExpr.Range().Start.Byte > pos.Byte {
break
}
// we're not checking the end byte position here, because we don't
// allow completion after the "}"
if partExpr.Range().ContainsPos(pos) || partExpr.Range().End.Byte == pos.Byte {
return e.complete(partExpr, &schema.AttributeSchema{Constraint: schema.String{}})
}
// trailing dot may be ignored by the parser so we attempt to recover it
if pos.Byte-partExpr.Range().End.Byte == 1 {
fileBytes := e.ctx.FileBytes(partExpr)
trailingRune := fileBytes[partExpr.Range().End.Byte:pos.Byte][0]
if trailingRune == '.' {
return e.complete(partExpr, &schema.AttributeSchema{Constraint: schema.String{}})
}
}
}
return nil
}
package completion
import (
"reflect"
"strings"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/funchcl/decoder"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/lang"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/schema"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/writer"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/function"
)
// expressionCompleter provides completion information for expressions.
type expressionCompleter struct {
extractor
ctx decoder.CompletionContext
pos hcl.Pos
depth int
}
// newExpressionCompleter creates an expressionCompleter.
func newExpressionCompleter(ctx decoder.CompletionContext, pos hcl.Pos) *expressionCompleter {
return &expressionCompleter{extractor: extractor{ctx: ctx}, ctx: ctx, pos: pos}
}
func (e *expressionCompleter) inCompletionRange(expr hclsyntax.Expression) bool {
r := expr.Range()
return r.ContainsPos(e.pos) || r.End.Byte == e.pos.Byte
}
func compressString(s string, maxLength int) string {
if len(s) > maxLength && maxLength > 8 {
s = s[:maxLength-8] + " ... " + s[len(s)-2:]
}
return strings.ReplaceAll(s, "\n", " ")
}
// complete returns completion data for the supplied expression having the supplied schema.
func (e *expressionCompleter) complete(expr hclsyntax.Expression, s *schema.AttributeSchema) []lang.Candidate {
descend := func(eses ...exprSchema) []lang.Candidate {
for _, es := range eses {
if es.expr == nil {
continue
}
if e.inCompletionRange(es.expr) {
return e.complete(es.expr, es.schema)
}
}
return nil
}
if debugCompletion {
str := compressString(writer.NodeToSource(expr.(hclsyntax.Node)), 60)
debugLogger.Printf("%-80s %s %s\n",
strings.Repeat(" ", e.depth)+str,
strings.TrimPrefix(reflect.TypeOf(expr).String(), "*hclsyntax."),
s.Constraint.FriendlyName(),
)
}
e.depth++
defer func() { e.depth-- }()
pos := e.pos
if len(s.CompletionHooks) > 0 {
candidates := e.candidatesFromHooks(expr, s)
if len(candidates) > 0 {
return candidates
}
}
if isEmptyExpression(expr) {
return e.completeEmptyExpression(expr, s)
}
switch expr := expr.(type) {
case *hclsyntax.ExprSyntaxError:
return e.standardRefs(expr, s)
case *hclsyntax.ScopeTraversalExpr:
return e.standardRefs(expr, s)
case *hclsyntax.ObjectConsExpr:
objCons, ok := s.Constraint.(schema.Object)
if !ok {
objCons = schema.Object{Attributes: map[string]*schema.AttributeSchema{}}
}
return e.completeObject(expr, objCons)
// for tuples we simply descend the list.
case *hclsyntax.TupleConsExpr:
itemSchema := unknownSchema
if listCons, ok := s.Constraint.(schema.List); ok {
itemSchema = &schema.AttributeSchema{Constraint: listCons.Elem}
}
for _, ce := range expr.Exprs {
if e.inCompletionRange(ce) {
return e.complete(ce, itemSchema)
}
}
// TODO: figure out how to process these
case *hclsyntax.RelativeTraversalExpr:
case *hclsyntax.IndexExpr:
case *hclsyntax.SplatExpr:
case *hclsyntax.FunctionCallExpr:
funcSig, knownFunc := e.ctx.Functions()[expr.Name]
if !knownFunc {
funcSig = schema.FunctionSignature{
Description: "unknown function",
ReturnType: cty.DynamicPseudoType,
VarParam: &function.Parameter{
Name: "unknown",
Type: cty.DynamicPseudoType,
},
}
}
if expr.NameRange.ContainsPos(pos) {
return nil // TODO: list other functions matching prefix at cursor
}
// special processing for merge: make args inherit the LHS schema
if expr.Name == "merge" {
return descend(withExpressionsOfSchema(s, expr.Args...)...)
}
for i, arg := range expr.Args {
if !e.inCompletionRange(arg) {
continue
}
argSchema := unknownSchema
if knownFunc {
if i < len(funcSig.Params) {
argSchema = schemaForType(funcSig.Params[i].Type)
} else if funcSig.VarParam != nil {
argSchema = schemaForType(funcSig.VarParam.Type)
}
}
return e.complete(arg, argSchema)
}
case *hclsyntax.TemplateExpr:
return e.completeTemplateExpr(expr)
case *hclsyntax.ForExpr:
// TODO: for expressions!
// simpler descents for the remaining types.
case *hclsyntax.ConditionalExpr:
return descend(
withExpressionSchema(expr.Condition, boolSchema),
withExpressionSchema(expr.TrueResult, s),
withExpressionSchema(expr.FalseResult, s),
)
case *hclsyntax.BinaryOpExpr:
switch expr.Op {
case hclsyntax.OpAdd, hclsyntax.OpSubtract, hclsyntax.OpMultiply, hclsyntax.OpDivide, hclsyntax.OpModulo:
return descend(withExpressionsOfSchema(numberSchema, expr.LHS, expr.RHS)...)
case hclsyntax.OpLogicalAnd, hclsyntax.OpLogicalOr, hclsyntax.OpLogicalNot:
return descend(withExpressionsOfSchema(boolSchema, expr.LHS, expr.RHS)...)
}
return descend(withUnknownExpressions(expr.LHS, expr.RHS)...)
case *hclsyntax.UnaryOpExpr:
return descend(withExpressionSchema(expr.Val, schemaForType(expr.Op.Type)))
case *hclsyntax.TemplateWrapExpr:
return descend(withExpressionSchema(expr.Wrapped, s))
case *hclsyntax.ParenthesesExpr:
return descend(withExpressionSchema(expr.Expression, s))
case *hclsyntax.ObjectConsKeyExpr:
// no-ops: there is no valuable completion that can be provided for these expressions.
case *hclsyntax.LiteralValueExpr:
case *hclsyntax.AnonSymbolExpr:
}
return nil
}
// candidatesFromHooks returns hook candidates at the supplied position, correctly accounting for
// incomplete expressions and parse failures.
func (e *expressionCompleter) candidatesFromHooks(expr hcl.Expression, aSchema *schema.AttributeSchema) []lang.Candidate {
return candidatesFromHooks(e.ctx, expr, aSchema, e.pos)
}
func (e *expressionCompleter) standardRefs(expr hclsyntax.Expression, s *schema.AttributeSchema) []lang.Candidate {
var candidates []lang.Candidate
candidates = append(candidates, e.completeRef(expr, s)...)
candidates = append(candidates, e.completeFunction(expr, s)...)
return candidates
}
func (e *expressionCompleter) completeEmptyExpression(expr hclsyntax.Expression, s *schema.AttributeSchema) []lang.Candidate {
pos := e.pos
// TODO: literal booleans
returnRefs := func() []lang.Candidate {
return e.standardRefs(expr, s)
}
switch cons := s.Constraint.(type) {
// we don't have these use-cases yet, but keeping the placeholder here
case schema.TypeDeclaration:
return nil
case schema.Object:
cData := cons.EmptyCompletionData(1, 0)
return []lang.Candidate{{
Label: "{…}",
Detail: "object",
Kind: lang.ObjectCandidateKind,
Description: cons.Description,
TextEdit: lang.TextEdit{
NewText: cData.NewText,
Snippet: cData.Snippet,
Range: hcl.Range{
Filename: expr.Range().Filename,
Start: pos,
End: pos,
},
},
TriggerSuggest: cData.TriggerSuggest,
}}
case schema.List:
d := cons.EmptyCompletionData(1, 0)
return []lang.Candidate{{
Label: "[ ]",
Detail: cons.FriendlyName(),
Kind: lang.ListCandidateKind,
Description: cons.Description,
TextEdit: lang.TextEdit{
NewText: d.NewText,
Snippet: d.Snippet,
Range: hcl.Range{
Filename: expr.Range().Filename,
Start: pos,
End: pos,
},
},
TriggerSuggest: d.TriggerSuggest,
}}
default:
return returnRefs()
}
}
package completion
import (
"fmt"
"strings"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/schema"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
)
func (c *Completer) fileByName(name string) (*hcl.File, error) {
f, ok := c.ctx.HCLFileByName(name)
if !ok {
return nil, &fileNotFoundError{filename: name}
}
return f, nil
}
func (c *Completer) bodyForFileAndPos(name string, f *hcl.File, pos hcl.Pos) (*hclsyntax.Body, error) {
body := f.Body.(*hclsyntax.Body)
//nolint:staticcheck
if !(body.Range().ContainsPos(pos) ||
posEqual(body.Range().Start, pos) ||
posEqual(body.Range().End, pos)) {
return nil, &posOutOfRangeError{
filename: name,
pos: pos,
rng: body.Range(),
}
}
return body, nil
}
func (c *Completer) bytesFromRange(rng hcl.Range) ([]byte, error) {
b, err := c.bytesForFile(rng.Filename)
if err != nil {
return nil, err
}
return rng.SliceBytes(b), nil
}
func (c *Completer) bytesForFile(file string) ([]byte, error) {
b, ok := c.ctx.FileBytesByName(file)
if !ok {
return nil, &fileNotFoundError{filename: file}
}
return b, nil
}
func (c *Completer) nameTokenRangeAtPos(filename string, pos hcl.Pos) (hcl.Range, error) {
rng := hcl.Range{
Filename: filename,
Start: pos,
End: pos,
}
f, err := c.fileByName(filename)
if err != nil {
return rng, err
}
tokens, diags := hclsyntax.LexConfig(f.Bytes, filename, hcl.InitialPos)
if diags.HasErrors() {
return rng, diags
}
return nameTokenRangeAtPos(tokens, pos)
}
func nameTokenRangeAtPos(tokens hclsyntax.Tokens, pos hcl.Pos) (hcl.Range, error) {
// TODO: understand wtf is happening here
for i, t := range tokens {
if t.Range.ContainsPos(pos) {
if t.Type == hclsyntax.TokenIdent {
return t.Range, nil
}
if t.Type == hclsyntax.TokenNewline && i > 0 {
// end of line
previousToken := tokens[i-1]
if previousToken.Type == hclsyntax.TokenIdent {
return previousToken.Range, nil
}
}
return hcl.Range{}, fmt.Errorf("token is %s, not Ident", t.Type.String())
}
// EOF token has zero length
// so we just compare start/end position
if t.Type == hclsyntax.TokenEOF && t.Range.Start == pos && t.Range.End == pos && i > 0 {
previousToken := tokens[i-1]
if previousToken.Type == hclsyntax.TokenIdent {
return previousToken.Range, nil
}
}
}
return hcl.Range{}, fmt.Errorf("no token found at %s", posToStr(pos))
}
func isPosOutsideBody(block *hclsyntax.Block, pos hcl.Pos) bool {
if block.OpenBraceRange.ContainsPos(pos) {
return true
}
if block.CloseBraceRange.ContainsPos(pos) {
return true
}
if hcl.RangeBetween(block.TypeRange, block.OpenBraceRange).ContainsPos(pos) {
return true
}
return false
}
// detailForBlock returns a `Detail` info string to display in an editor in a hover event
func detailForBlock(_ *schema.BasicBlockSchema) string {
detail := "Block"
return strings.TrimSpace(detail)
}
// snippetForBlock takes a block and returns a formatted snippet for a user to complete inside an editor.
func snippetForBlock(blockType string, block *schema.BasicBlockSchema) string {
labels := ""
placeholder := 0
for _, l := range block.Labels {
placeholder++
if l.CanComplete() {
labels += fmt.Sprintf(` ${%d|%s|}`, placeholder, strings.Join(l.AllowedValues, ","))
} else {
labels += fmt.Sprintf(` ${%d:%s}`, placeholder, l.Name)
}
}
return fmt.Sprintf("%s%s {\n $0\n}", blockType, labels)
}
package completion
import (
"fmt"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/funchcl/decoder"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/lang"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/schema"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
)
// expressionHover provides hover information for expressions.
type expressionHover struct {
extractor
ctx decoder.CompletionContext
pos hcl.Pos
depth int
}
// newExpressionHover creates an expressionHover.
func newExpressionHover(ctx decoder.CompletionContext, pos hcl.Pos) *expressionHover {
return &expressionHover{extractor: extractor{ctx: ctx}, ctx: ctx, pos: pos}
}
// hover returns hover data for the supplied expression having the supplied schema. In many cases, this will
// return good hover content even when the schema is unknown based on the actual type of the underlying
// expression, if it is possible to compute this without the schema. For instance, we know the types of
// most local variables since this is inferred and present in the target schema tree.
func (e *expressionHover) hover(s *schema.AttributeSchema, expr hclsyntax.Expression) *lang.HoverData {
descend := func(eses ...exprSchema) *lang.HoverData {
for _, es := range eses {
if es.expr == nil {
continue
}
if es.expr.Range().ContainsPos(e.pos) {
return e.hover(es.schema, es.expr)
}
}
return nil
}
e.depth++
defer func() { e.depth-- }()
pos := e.pos
switch expr := expr.(type) {
// for objects, we need to understand whether we are positioned on an attribute name or its value.
// When positioned on the name, we extract the schema from the supplied schema passed in and, if
// this is unknown, we infer its type from the expression to which it is assigned. If positioned
// on a value, we simply process the expression.
case *hclsyntax.ObjectConsExpr:
objCons, ok := s.Constraint.(schema.Object)
if !ok {
objCons = schema.Object{Attributes: map[string]*schema.AttributeSchema{}}
}
for _, item := range expr.Items {
if !item.KeyExpr.Range().ContainsPos(pos) && !item.ValueExpr.Range().ContainsPos(pos) {
continue
}
itemSchema := unknownSchema
key, _, found := rawObjectKey(item.KeyExpr)
if found && key != "" {
as, ok1 := objCons.Attributes[key]
if ok1 {
itemSchema = as
}
}
if item.KeyExpr.Range().ContainsPos(pos) {
if itemSchema == unknownSchema {
itemSchema = e.impliedSchema(item.ValueExpr)
}
if itemSchema != unknownSchema {
return &lang.HoverData{
Content: hoverContentForAttribute(key, itemSchema),
Range: item.KeyExpr.Range(),
}
}
}
if item.ValueExpr.Range().ContainsPos(pos) {
return e.hover(itemSchema, item.ValueExpr)
}
}
// for tuples we simply descend the list.
case *hclsyntax.TupleConsExpr:
itemSchema := unknownSchema
if listCons, ok := s.Constraint.(schema.List); ok {
itemSchema = &schema.AttributeSchema{Constraint: listCons.Elem}
}
for _, ce := range expr.Exprs {
if ce.Range().ContainsPos(pos) {
return e.hover(itemSchema, ce)
}
}
// return hover content for the attribute on which the cursor hovers.
case *hclsyntax.ScopeTraversalExpr:
ti := e.extractTraversal(e.ctx.TargetSchema(), expr, expr.Traversal, e.pos)
if ti == nil {
return nil
}
return &lang.HoverData{
Content: hoverContentForAttribute(ti.source, ti.schema),
Range: ti.rng,
}
// ditto for relative traversals, but compute source schema first.
case *hclsyntax.RelativeTraversalExpr:
if expr.SrcRange.ContainsPos(pos) {
return e.hover(unknownSchema, expr.Source)
}
rootSchema := e.impliedSchema(expr.Source)
ti := e.extractTraversal(rootSchema, expr, expr.Traversal, e.pos)
if ti == nil {
return nil
}
return &lang.HoverData{
Content: hoverContentForAttribute(ti.source, ti.schema),
Range: ti.rng,
}
// ditto
case *hclsyntax.IndexExpr:
if expr.SrcRange.ContainsPos(pos) {
return e.hover(unknownSchema, expr.Collection)
}
if expr.Key.Range().ContainsPos(pos) {
sch := e.impliedSchema(expr.Key)
if sch != unknownSchema {
return &lang.HoverData{
Content: hoverContentForAttribute(string(expr.Key.Range().SliceBytes(e.ctx.FileBytes(expr))), sch),
Range: expr.Key.Range(),
}
}
}
// if cursor on relative part, unwrap the LHS splat and re-wrap at the end.
case *hclsyntax.SplatExpr:
if expr.Source.Range().ContainsPos(pos) || expr.MarkerRange.ContainsPos(pos) {
return e.hover(unknownSchema, expr.Source)
}
rel, isRel := expr.Each.(*hclsyntax.RelativeTraversalExpr)
if !isRel {
return nil
}
splatSchema := e.impliedSchema(expr.Source)
listCons, isList := splatSchema.Constraint.(schema.List)
var rootSchema *schema.AttributeSchema
if !isList {
return nil
}
rootSchema = schemaForConstraint(listCons.Elem)
ti := e.extractTraversal(rootSchema, expr, rel.Traversal, e.pos)
if ti == nil {
return nil
}
ti.schema = schemaForConstraint(schema.List{Elem: ti.schema.Constraint})
return &lang.HoverData{
Content: hoverContentForAttribute(ti.source, ti.schema),
Range: ti.rng,
}
// for function calls, if positioned on the name return the function signature.
// else infer a type for the argument on which the cursor hovers and use that.
// Special case for the `merge` function: arguments are assigned to the LHS
// attribute schema if known.
case *hclsyntax.FunctionCallExpr:
funcSig, knownFunc := e.ctx.Functions()[expr.Name]
if expr.NameRange.ContainsPos(pos) {
if !knownFunc {
break
}
return &lang.HoverData{
Content: hoverContentForFunction(expr.Name, funcSig),
Range: expr.NameRange,
}
}
if expr.Name == "merge" {
return descend(withExpressionsOfSchema(s, expr.Args...)...)
}
for i, arg := range expr.Args {
if !arg.Range().ContainsPos(pos) {
continue
}
argSchema := unknownSchema
if knownFunc {
if i < len(funcSig.Params) {
argSchema = schemaForType(funcSig.Params[i].Type)
} else if funcSig.VarParam != nil {
argSchema = schemaForType(funcSig.VarParam.Type)
}
}
return e.hover(argSchema, arg)
}
case *hclsyntax.ForExpr:
// TODO: for expressions!
// simpler descents for the remaining types.
case *hclsyntax.TemplateExpr:
return descend(withExpressionsOfSchema(stringSchema, expr.Parts...)...)
case *hclsyntax.ConditionalExpr:
return descend(
withExpressionSchema(expr.Condition, boolSchema),
withExpressionSchema(expr.TrueResult, s),
withExpressionSchema(expr.FalseResult, s),
)
case *hclsyntax.BinaryOpExpr:
switch expr.Op {
case hclsyntax.OpAdd, hclsyntax.OpSubtract, hclsyntax.OpMultiply, hclsyntax.OpDivide, hclsyntax.OpModulo:
return descend(withExpressionsOfSchema(numberSchema, expr.LHS, expr.RHS)...)
case hclsyntax.OpLogicalAnd, hclsyntax.OpLogicalOr, hclsyntax.OpLogicalNot:
return descend(withExpressionsOfSchema(boolSchema, expr.LHS, expr.RHS)...)
}
return descend(withUnknownExpressions(expr.LHS, expr.RHS)...)
case *hclsyntax.UnaryOpExpr:
return descend(withExpressionSchema(expr.Val, schemaForType(expr.Op.Type)))
case *hclsyntax.TemplateWrapExpr:
return descend(withExpressionSchema(expr.Wrapped, s))
case *hclsyntax.ParenthesesExpr:
return descend(withExpressionSchema(expr.Expression, s))
// no-ops: there is no valuable hover that can be provided for these expressions.
case *hclsyntax.ObjectConsKeyExpr:
case *hclsyntax.LiteralValueExpr:
case *hclsyntax.AnonSymbolExpr:
case *hclsyntax.ExprSyntaxError:
}
return nil
}
func hoverContentForFunction(name string, funcSig schema.FunctionSignature) lang.MarkupContent {
rawMd := fmt.Sprintf("```\n%s(%s) %s\n```\n\n%s",
name, parameterNamesAsString(funcSig), funcSig.ReturnType.FriendlyName(), funcSig.Description)
if funcSig.Detail != "" {
rawMd += fmt.Sprintf("\n\n%s", funcSig.Detail)
}
return lang.Markdown(rawMd)
}
package completion
import (
"fmt"
"strings"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/lang"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/schema"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
)
func (c *Completer) doHover(filename string, pos hcl.Pos) (*lang.HoverData, error) {
f, err := c.fileByName(filename)
if err != nil {
return nil, err
}
rootBody, err := c.bodyForFileAndPos(filename, f, pos)
if err != nil {
return nil, err
}
// log.Printf("using source:\n%s\n", writer.NodeToSource(rootBody))
data, err := c.hoverAtPos(rootBody, schema.NewBlockStack(), pos)
if err != nil {
return nil, err
}
return data, nil
}
func (c *Completer) hoverAtPos(body *hclsyntax.Body, bs schema.BlockStack, pos hcl.Pos) (*lang.HoverData, error) {
if body == nil {
return nil, fmt.Errorf("hoverAtPos: body is nil")
}
filename := body.Range().Filename
// process position inside an attribute
for name, attr := range body.Attributes {
if !attr.Range().ContainsPos(pos) {
continue
}
aSchema := c.ctx.AttributeSchema(bs, attr.Name)
if attr.NameRange.ContainsPos(pos) {
impliedSchema := c.ctx.ImpliedAttributeSchema(bs, attr.Name)
if impliedSchema == nil {
impliedSchema = aSchema
}
return &lang.HoverData{
Content: hoverContentForAttribute(name, impliedSchema),
Range: attr.Range(),
}, nil
}
if attr.Expr.Range().ContainsPos(pos) {
// return newExpr(attr.Expr, 0, aSchema.Constraint).HoverAtPos(c.ctx, pos), nil
eh := newExpressionHover(c.ctx, pos)
return eh.hover(aSchema, attr.Expr), nil
}
}
for _, block := range body.Blocks {
if !block.Range().ContainsPos(pos) {
continue
}
parentSchema := c.ctx.BodySchema(bs)
bs.Push(block)
labelSchemas := c.ctx.LabelSchema(bs)
bodySchema := c.ctx.BodySchema(bs)
if bodySchema == nil {
return nil, fmt.Errorf("unknown block type %q", block.Type)
}
if block.TypeRange.ContainsPos(pos) {
blockSchema := parentSchema.NestedBlocks[block.Type]
if blockSchema == nil {
return nil, fmt.Errorf("unknown block type %q", bs.Peek(1).Type)
}
return &lang.HoverData{
Content: c.hoverContentForBlock(block.Type, blockSchema),
Range: block.TypeRange,
}, nil
}
for i, labelRange := range block.LabelRanges {
if labelRange.ContainsPos(pos) || posEqual(labelRange.End, pos) {
if i+1 > len(labelSchemas) {
return nil, &positionalError{
filename: filename,
pos: pos,
msg: fmt.Sprintf("unexpected label (%d) %q", i, block.Labels[i]),
}
}
return &lang.HoverData{
Content: c.hoverContentForLabel(labelSchemas[i], block.Labels[i]),
Range: labelRange,
}, nil
}
}
if isPosOutsideBody(block, pos) {
return nil, &positionalError{
filename: filename,
pos: pos,
msg: fmt.Sprintf("position outside of %q body", block.Type),
}
}
return c.hoverAtPos(block.Body, bs, pos)
}
// Position outside any attribute or block
return nil, &positionalError{
filename: filename,
pos: pos,
msg: "position outside of any attribute name, value or block",
}
}
func (c *Completer) hoverContentForBlock(bType string, schema *schema.BasicBlockSchema) lang.MarkupContent {
value := fmt.Sprintf("**%s** _%s_%s", bType, detailForBlock(schema), schema.Description.AsDetail())
return lang.NewMarkup(lang.MarkdownKind, value)
}
func (c *Completer) hoverContentForLabel(labelSchema *schema.LabelSchema, value string) lang.MarkupContent {
content := fmt.Sprintf("%q", value)
if labelSchema.Name != "" {
content += fmt.Sprintf(" (%s)", labelSchema.Name)
}
content = strings.TrimSpace(content)
content += labelSchema.Description.AsDetail()
return lang.Markdown(content)
}
package completion
import (
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/funchcl/decoder"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/funchcl/target"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/funchcl/typeutils"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/schema"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/zclconf/go-cty/cty"
)
// extractor extracts interesting information from expressions.
type extractor struct {
ctx decoder.CompletionContext
}
// traversalInfo holds the result of inspecting a (possibly partial) traversal.
type traversalInfo struct {
source string
rng hcl.Range
schema *schema.AttributeSchema
}
// extractTraversal extracts traversal information for the supplied traversal and hover position.
// For example, if the expression is `a.b.c` and the user positions on `b`, then information for `a.b`
// is returned.
func (e *extractor) extractTraversal(rootSchema *schema.AttributeSchema, expr hclsyntax.Expression,
t hcl.Traversal, pos hcl.Pos) *traversalInfo {
foundIndex := -1
for i, t := range t {
if t.SourceRange().ContainsPos(pos) {
foundIndex = i
break
}
}
if foundIndex == -1 {
return nil
}
wantTraversals := t[:foundIndex+1]
sourceRange := hcl.RangeBetween(expr.StartRange(), t[foundIndex].SourceRange())
sc := target.SchemaForRelativeTraversal(rootSchema, wantTraversals)
return &traversalInfo{
source: string(sourceRange.SliceBytes(e.ctx.FileBytes(expr))),
rng: sourceRange,
schema: sc,
}
}
// impliedSchema returns a schema for the supplied expression or an unknown schema.
func (e *extractor) impliedSchema(expr hclsyntax.Expression) *schema.AttributeSchema {
switch expr := expr.(type) {
// process a scope traversal fully or partially by taking the cursor position into account.
case *hclsyntax.ScopeTraversalExpr:
pos := hcl.Pos{
Line: expr.Range().End.Line,
Column: expr.Range().End.Column,
Byte: expr.Range().End.Byte - 1,
}
ti := e.extractTraversal(e.ctx.TargetSchema(), expr, expr.Traversal, pos)
if ti == nil {
return unknownSchema
}
return ti.schema
// ditto for a relative traversal except we need to compute the base implied schema first.
case *hclsyntax.RelativeTraversalExpr:
rootSchema := e.impliedSchema(expr.Source)
return target.SchemaForRelativeTraversal(rootSchema, expr.Traversal)
// for a splat expression we get the schema for the LHS. Then we need
// to unwrap a list schema, calculate the schema relative to the unwrapped
// schema and re-wrap the result into a list.
case *hclsyntax.SplatExpr:
checkRootSchema := e.impliedSchema(expr.Source)
unwrapSchema := unknownSchema
if s, ok := checkRootSchema.Constraint.(schema.List); ok {
unwrapSchema = schemaForConstraint(s.Elem)
}
switch expr := expr.Each.(type) {
case *hclsyntax.RelativeTraversalExpr:
s := target.SchemaForRelativeTraversal(unwrapSchema, expr.Traversal)
return schemaForConstraint(schema.List{Elem: s.Constraint})
}
// for an index expression we calculate the schema on the left. For maps,
// and lists we don't care about the value of the index key and simply unwrap it.
// For objects, we try and figure out what the index key is, if possible,
// and return that subschema.
case *hclsyntax.IndexExpr:
rootSchema := e.impliedSchema(expr.Collection)
switch cons := rootSchema.Constraint.(type) {
case schema.List:
return schemaForConstraint(cons.Elem)
case schema.Map:
return schemaForConstraint(cons.Elem)
case schema.Object:
v, diags := expr.Key.Value(nil)
if diags.HasErrors() || !v.IsWhollyKnown() || v.Type() != cty.String {
return unknownSchema
}
key := v.AsString()
attrSchema, ok := cons.Attributes[key]
if !ok {
return unknownSchema
}
return attrSchema
}
// for an object constructor we return an object schema filled in to the extent possible.
case *hclsyntax.ObjectConsExpr:
attrs := map[string]*schema.AttributeSchema{}
for _, item := range expr.Items {
key, _, found := rawObjectKey(item.KeyExpr)
if !found {
continue
}
valSchema := e.impliedSchema(item.ValueExpr)
attrs[key] = valSchema
}
return schemaForConstraint(schema.Object{Attributes: attrs})
// literal schemas are reverse-engineered from their type.
case *hclsyntax.LiteralValueExpr:
return schemaForType(expr.Val.Type())
// template expressions are, by definition, strings.
case *hclsyntax.TemplateExpr:
return stringSchema
// for a function call, the implied schema is for its return type.
case *hclsyntax.FunctionCallExpr:
funcSig, knownFunc := e.ctx.Functions()[expr.Name]
if !knownFunc {
break
}
return schemaForType(funcSig.ReturnType)
}
// and we don't know how to process anything else.
return unknownSchema
}
var (
unknownConstraint = schema.Any{}
unknownSchema = &schema.AttributeSchema{Constraint: unknownConstraint}
boolSchema = &schema.AttributeSchema{Constraint: schema.Bool{}}
stringSchema = &schema.AttributeSchema{Constraint: schema.String{}}
numberSchema = &schema.AttributeSchema{Constraint: schema.Number{}}
)
func constraintForType(t cty.Type) schema.Constraint {
return typeutils.TypeConstraint(t)
}
// schemaForConstraint returns an attribute schema that wraps the supplied constraint.
func schemaForConstraint(t schema.Constraint) *schema.AttributeSchema {
return &schema.AttributeSchema{Constraint: t}
}
// schemaForType returns a schema for a value of the specified type
func schemaForType(t cty.Type) *schema.AttributeSchema {
return schemaForConstraint(constraintForType(t))
}
// exprSchema pairs an expression with an attribute schema.
type exprSchema struct {
expr hclsyntax.Expression
schema *schema.AttributeSchema
}
// withExpressionSchema returns an expSchema for the supplied expression and schema.
func withExpressionSchema(e hclsyntax.Expression, s *schema.AttributeSchema) exprSchema {
return exprSchema{expr: e, schema: s}
}
// withUnknownExpression returns an expSchema for the supplied expression and an unknown schema.
func withUnknownExpression(e hclsyntax.Expression) exprSchema {
return exprSchema{expr: e, schema: unknownSchema}
}
// withUnknownExpressions returns a list of expSchema for the supplied expressions and an unknown schema.
func withUnknownExpressions(e ...hclsyntax.Expression) []exprSchema {
var ret []exprSchema
for _, e := range e {
ret = append(ret, withUnknownExpression(e))
}
return ret
}
// withExpressionsOfSchema returns a list of expSchema for the supplied schema an expression list.
func withExpressionsOfSchema(s *schema.AttributeSchema, e ...hclsyntax.Expression) []exprSchema {
var ret []exprSchema
for _, e := range e {
ret = append(ret, withExpressionSchema(e, s))
}
return ret
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package completion
import (
"bytes"
"fmt"
"strings"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/lang"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/schema"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
)
// SignatureAtPos returns a function signature for the given pos if pos
// is inside a FunctionCallExpr
func (c *Completer) SignatureAtPos(filename string, pos hcl.Pos) (*lang.FunctionSignature, error) {
file, ok := c.ctx.HCLFileByName(filename)
if !ok {
return nil, &fileNotFoundError{filename: filename}
}
body, err := c.bodyForFileAndPos(filename, file, pos)
if err != nil {
return nil, err
}
var signature *lang.FunctionSignature
_ = hclsyntax.VisitAll(body, func(node hclsyntax.Node) hcl.Diagnostics {
if !node.Range().ContainsPos(pos) {
return nil
}
fNode, isFunc := node.(*hclsyntax.FunctionCallExpr)
if !isFunc {
return nil
}
f, ok := c.ctx.Functions()[fNode.Name]
if !ok {
return nil
}
if len(f.Params) == 0 && f.VarParam == nil {
signature = &lang.FunctionSignature{
Name: fmt.Sprintf("%s(%s) %s", fNode.Name, parameterNamesAsString(f), f.ReturnType.FriendlyName()),
Description: lang.Markdown(f.Description),
}
return nil // function accepts no parameters, return early
}
pRange := hcl.RangeBetween(fNode.OpenParenRange, fNode.CloseParenRange)
if !pRange.ContainsPos(pos) {
return nil // not inside parenthesis
}
activePar := 0 // default to first parameter
foundActivePar := false
lastArgEndPos := fNode.OpenParenRange.Start
lastArgIdx := 0
for i, v := range fNode.Args {
// we overshot the argument and stop
if v.Range().Start.Byte > pos.Byte {
break
}
if v.Range().ContainsPos(pos) || v.Range().End.Byte == pos.Byte {
activePar = i
foundActivePar = true
break
}
lastArgEndPos = v.Range().End
lastArgIdx = i
}
if !foundActivePar {
recoveredBytes := recoverLeftBytes(file.Bytes, pos, func(byteOffset int, r rune) bool {
return r == ',' && byteOffset > lastArgEndPos.Byte
})
trimmedBytes := bytes.TrimRight(recoveredBytes, " \t\n")
if string(trimmedBytes) == "," {
activePar = lastArgIdx + 1
}
}
paramsLen := len(f.Params)
if f.VarParam != nil {
paramsLen += 1
}
if activePar >= paramsLen && f.VarParam == nil {
return nil // too many arguments passed to the function
}
if activePar >= paramsLen {
// there are multiple variadic arguments passed, so
// we want to highlight the variadic parameter in the
// function signature
activePar = paramsLen - 1
}
parameters := make([]lang.FunctionParameter, 0, paramsLen)
for _, p := range f.Params {
parameters = append(parameters, lang.FunctionParameter{
Name: p.Name,
Description: lang.Markdown(p.Description),
})
}
if f.VarParam != nil {
parameters = append(parameters, lang.FunctionParameter{
Name: f.VarParam.Name,
Description: lang.Markdown(f.VarParam.Description),
})
}
signature = &lang.FunctionSignature{
Name: fmt.Sprintf("%s(%s) %s", fNode.Name, parameterNamesAsString(f), f.ReturnType.FriendlyName()),
Description: lang.Markdown(f.Description),
Parameters: parameters,
ActiveParameter: uint32(activePar),
}
return nil
})
return signature, nil
}
// parameterNamesAsString returns a string containing all function parameters
// with their respective types.
//
// Useful for displaying as part of a function signature.
func parameterNamesAsString(fs schema.FunctionSignature) string {
paramsLen := len(fs.Params)
if fs.VarParam != nil {
paramsLen += 1
}
names := make([]string, 0, paramsLen)
for _, p := range fs.Params {
names = append(names, fmt.Sprintf("%s %s", p.Name, p.Type.FriendlyName()))
}
if fs.VarParam != nil {
names = append(names, fmt.Sprintf("…%s %s", fs.VarParam.Name, fs.VarParam.Type.FriendlyName()))
}
return strings.Join(names, ", ")
}
package completion
import (
"fmt"
"sort"
"strings"
"unicode"
"unicode/utf8"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/funchcl/decoder"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/funchcl/decoder/decoderutils"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/lang"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/schema"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/hashicorp/hcl/v2/json"
"github.com/zclconf/go-cty/cty"
)
// isEmptyExpression returns true if given expression is suspected
// to be empty, e.g. newline after equal sign.
//
// Because upstream HCL parser doesn't always handle incomplete
// configuration gracefully, this may not cover all cases.
func isEmptyExpression(expr hcl.Expression) bool {
l, ok := expr.(*hclsyntax.LiteralValueExpr)
if !ok {
return false
}
if l.Val != cty.DynamicVal {
return false
}
return true
}
// newEmptyExpressionAtPos returns a new "artificial" empty expression
// which can be used during completion inside another expression
// in an empty space which isn't already represented by empty expression.
//
// For example, new argument after comma in function call,
// or new element in a list or set.
func newEmptyExpressionAtPos(filename string, pos hcl.Pos) hcl.Expression {
return &hclsyntax.LiteralValueExpr{
Val: cty.DynamicVal,
SrcRange: hcl.Range{
Filename: filename,
Start: pos,
End: pos,
},
}
}
// recoverLeftBytes seeks left from given pos in given slice of bytes
// and recovers all bytes up until f matches, including that match.
// This allows recovery of incomplete configuration which is not
// present in the parsed AST during completion.
//
// Zero bytes is returned if no match was found.
func recoverLeftBytes(b []byte, pos hcl.Pos, f func(byteOffset int, r rune) bool) []byte {
firstRune, size := utf8.DecodeLastRune(b[:pos.Byte])
offset := pos.Byte - size
// check for early match
if f(pos.Byte, firstRune) {
return b[offset:pos.Byte]
}
for offset > 0 {
nextRune, size := utf8.DecodeLastRune(b[:offset])
if f(offset, nextRune) {
// record the matched offset
// and include the matched last rune
startByte := offset - size
return b[startByte:pos.Byte]
}
offset -= size
}
return []byte{}
}
// recoverRightBytes seeks right from given pos in given slice of bytes
// and recovers all bytes up until f matches, including that match.
// This allows recovery of incomplete configuration which is not
// present in the parsed AST during completion.
//
// Zero bytes is returned if no match was found.
func recoverRightBytes(b []byte, pos hcl.Pos, f func(byteOffset int, r rune) bool) []byte {
nextRune, size := utf8.DecodeRune(b[pos.Byte:])
offset := pos.Byte + size
// check for early match
if f(pos.Byte, nextRune) {
return b[pos.Byte:offset]
}
for offset < len(b) {
nextRune, size := utf8.DecodeRune(b[offset:])
if f(offset, nextRune) {
// record the matched offset
// and include the matched last rune
endByte := offset + size
return b[pos.Byte:endByte]
}
offset += size
}
return []byte{}
}
// isObjectItemTerminatingRune returns true if the given rune
// is considered a left terminating character for an item
// in hclsyntax.ObjectConsExpr.
func isObjectItemTerminatingRune(r rune) bool {
return r == '\n' || r == ',' || r == '{'
}
// isNamespacedFunctionNameRune returns true if the given rune
// is a valid character of a namespaced function name.
// This includes letters, digits, dashes, underscores, and colons.
func isNamespacedFunctionNameRune(r rune) bool {
return unicode.IsLetter(r) || unicode.IsDigit(r) || r == '-' || r == '_' || r == ':'
}
// rawObjectKey extracts raw key (as string) from KeyExpr of
// any hclsyntax.ObjectConsExpr along with the corresponding range
// and boolean indicating whether the extraction was successful.
//
// This accounts for the two common key representations (quoted and unquoted)
// and enables validation, filtering of object attributes and accurate
// calculation of edit range.
//
// It does *not* account for interpolation inside the key,
// such as { (var.key_name) = "foo" }.
func rawObjectKey(expr hcl.Expression) (string, *hcl.Range, bool) {
if json.IsJSONExpression(expr) {
val, diags := expr.Value(&hcl.EvalContext{})
if diags.HasErrors() {
return "", nil, false
}
if val.Type() != cty.String {
return "", nil, false
}
return val.AsString(), expr.Range().Ptr(), true
}
// regardless of what expression it is always wrapped
keyExpr, ok := expr.(*hclsyntax.ObjectConsKeyExpr)
if !ok {
return "", nil, false
}
switch eType := keyExpr.Wrapped.(type) {
// most common "naked" keys
case *hclsyntax.ScopeTraversalExpr:
if len(eType.Traversal) != 1 {
return "", nil, false
}
return eType.Traversal.RootName(), eType.Range().Ptr(), true
// less common quoted keys
case *hclsyntax.TemplateExpr:
if !eType.IsStringLiteral() {
return "", nil, false
}
// string literals imply exactly 1 part
lvExpr, ok := eType.Parts[0].(*hclsyntax.LiteralValueExpr)
if !ok {
return "", nil, false
}
if lvExpr.Val.Type() != cty.String {
return "", nil, false
}
return lvExpr.Val.AsString(), lvExpr.Range().Ptr(), true
}
return "", nil, false
}
// detailForAttribute provides additional information from the supplied attribute schema
// that can be displayed at hover, for example.
func detailForAttribute(attr *schema.AttributeSchema) string {
var details []string
if attr.IsRequired {
details = append(details, "*")
}
friendlyName := attr.Constraint.FriendlyName()
if friendlyName != "" {
details = append(details, friendlyName)
}
return strings.Join(details, " ")
}
// attributeSchemaToCandidate returns a completion candidate for an empty expression for the attribute name.
func attributeSchemaToCandidate(name string, attr *schema.AttributeSchema, rng hcl.Range) lang.Candidate {
var snippet string
var triggerSuggest bool
cData := attr.Constraint.EmptyCompletionData(1, 0)
snippet = fmt.Sprintf("%s = %s", name, cData.Snippet)
triggerSuggest = cData.TriggerSuggest
return lang.Candidate{
Label: name,
Detail: detailForAttribute(attr),
Description: attr.Description,
Kind: lang.AttributeCandidateKind,
TextEdit: lang.TextEdit{
NewText: name,
Snippet: snippet,
Range: rng,
},
TriggerSuggest: triggerSuggest,
}
}
// posEqual compares two positions for equality.
func posEqual(pos, other hcl.Pos) bool {
return pos.Line == other.Line &&
pos.Column == other.Column &&
pos.Byte == other.Byte
}
// posToStr returns a friendly representation of a position.
func posToStr(pos hcl.Pos) string {
return fmt.Sprintf("%d,%d", pos.Line, pos.Column)
}
// toCandidates adapts a list of hook candidates to completion candidates.
func toCandidates(in []lang.HookCandidate, editRange hcl.Range) []lang.Candidate {
out := make([]lang.Candidate, len(in))
for i, cd := range in {
out[i] = lang.Candidate{
Label: cd.Label,
Detail: cd.Detail,
Description: cd.Description,
Kind: cd.Kind,
IsDeprecated: cd.IsDeprecated,
TextEdit: lang.TextEdit{
NewText: cd.RawInsertText,
Snippet: cd.RawInsertText,
Range: editRange,
},
ResolveHook: cd.ResolveHook,
SortText: cd.SortText,
}
}
return out
}
// hoverContentForAttribute provides hover content for an attribute with the supplied name and schema.
func hoverContentForAttribute(name string, aSchema *schema.AttributeSchema) lang.MarkupContent {
value := fmt.Sprintf("**%s** _%s_", name, detailForAttribute(aSchema))
value += aSchema.Description.AsDetail()
if obj, ok := aSchema.Constraint.(schema.Object); ok {
if preview := objectAttributePreview(obj, "{", "}"); preview != "" {
value += "\n" + preview
}
} else if list, ok := aSchema.Constraint.(schema.List); ok {
if obj, ok := list.Elem.(schema.Object); ok {
if preview := objectAttributePreview(obj, "[{", "},...]"); preview != "" {
value += "\n" + preview
}
}
}
return lang.Markdown(value)
}
// objectAttributePreview returns a Markdown code block showing the attribute names
// and their types for the given object. If the object has more than 4 attributes,
// only the first 2 and last 2 are shown with "..." in between.
// The open and close parameters control the delimiters (e.g. "{" / "}" for objects,
// "[{" / "},...]" for lists of objects).
func objectAttributePreview(obj schema.Object, open, close string) string {
if len(obj.Attributes) == 0 {
return ""
}
names := make([]string, 0, len(obj.Attributes))
for n := range obj.Attributes {
names = append(names, n)
}
sort.Strings(names)
const maxInline = 4
var lines []string
lines = append(lines, open)
if len(names) <= maxInline {
for _, n := range names {
lines = append(lines, fmt.Sprintf(" %s: %s", n, obj.Attributes[n].Constraint.FriendlyName()))
}
} else {
for _, n := range names[:2] {
lines = append(lines, fmt.Sprintf(" %s: %s", n, obj.Attributes[n].Constraint.FriendlyName()))
}
lines = append(lines, " ...")
for _, n := range names[len(names)-2:] {
lines = append(lines, fmt.Sprintf(" %s: %s", n, obj.Attributes[n].Constraint.FriendlyName()))
}
}
lines = append(lines, close)
return "```\n" + strings.Join(lines, "\n") + "\n```"
}
// candidatesFromHooks returns hook candidates at the supplied position, correctly accounting for
// incomplete expressions and parse failures.
func candidatesFromHooks(ctx decoder.CompletionContext, expr hcl.Expression, aSchema *schema.AttributeSchema, pos hcl.Pos) []lang.Candidate {
var candidates []lang.Candidate
con, ok := aSchema.Constraint.(schema.TypeAwareConstraint)
if !ok {
// Return early as we only support string values for now
return candidates
}
typ, ok := con.ConstraintType()
if !ok || typ != cty.String {
// Return early as we only support string values for now
return candidates
}
editRng := expr.Range()
if isEmptyExpression(expr) || decoderutils.IsMultilineTemplateExpr(expr.(hclsyntax.Expression)) {
// An empty expression or a string without a closing quote will lead to
// an attribute expression spanning multiple lines.
// Since text edits only support a single line, we're resetting the End
// position here.
editRng.End = pos
}
prefixRng := expr.Range()
prefixRng.End = pos
prefixBytes := prefixRng.SliceBytes(ctx.FileBytes(expr))
prefix := string(prefixBytes)
prefix = strings.TrimLeft(prefix, `"`)
for _, hook := range aSchema.CompletionHooks {
if completionFunc := ctx.CompletionFunc(hook.Name); completionFunc != nil {
res, _ := completionFunc(decoder.CompletionFuncContext{
PathContext: ctx,
Dir: ctx.Dir(),
Filename: expr.Range().Filename,
Pos: pos,
}, prefix)
candidates = append(candidates, toCandidates(res, editRng)...)
}
}
return candidates
}
func boolLiteralTypeCandidates(prefix string, editRange hcl.Range) []lang.Candidate {
var candidates []lang.Candidate
if strings.HasPrefix("false", prefix) {
candidates = append(candidates, lang.Candidate{
Label: "false",
Detail: cty.Bool.FriendlyNameForConstraint(),
Kind: lang.BoolCandidateKind,
TextEdit: lang.TextEdit{
NewText: "false",
Snippet: "false",
Range: editRange,
},
})
}
if strings.HasPrefix("true", prefix) {
candidates = append(candidates, lang.Candidate{
Label: "true",
Detail: cty.Bool.FriendlyNameForConstraint(),
Kind: lang.BoolCandidateKind,
TextEdit: lang.TextEdit{
NewText: "true",
Snippet: "true",
Range: editRange,
},
})
}
return candidates
}
package decoderutils
import (
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/zclconf/go-cty/cty"
)
func IsMultilineStringLiteral(tplExpr *hclsyntax.TemplateExpr) bool {
if len(tplExpr.Parts) < 1 {
return false
}
for _, part := range tplExpr.Parts {
expr, ok := part.(*hclsyntax.LiteralValueExpr)
if !ok {
return false
}
if expr.Val.Type() != cty.String {
return false
}
}
return true
}
// IsMultilineTemplateExpr returns true if the expression is a template expression
// and spans more than one line.
func IsMultilineTemplateExpr(expr hclsyntax.Expression) bool {
t, ok := expr.(*hclsyntax.TemplateExpr)
if !ok {
return false
}
return t.Range().Start.Line != t.Range().End.Line
}
// Package folding provides folding range support for HCL files.
// This implementation is dependent on the calling extension (vscode versus intellij)
// that expect slightly different ranges for folding behavior.
package folding
import (
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/funchcl/decoder"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
)
// Range represents a foldable range in a document.
// Line and Column are 1-based (HCL convention).
type Range struct {
StartLine int
StartColumn int
EndLine int
EndColumn int
Kind string // "comment", "imports", or "region"
}
// Collect returns all foldable ranges for the given HCL file.
// It folds blocks and object expressions with '{' delimiters.
// Tuples/lists with '[]' are not folded due to lsp4ij limitations.
func Collect(file *hcl.File, b decoder.LangServerBehavior) []Range {
if file == nil {
return nil
}
body, ok := file.Body.(*hclsyntax.Body)
if !ok {
return nil
}
var ranges []Range
// Walk the AST and collect foldable nodes
_ = hclsyntax.VisitAll(body, func(node hclsyntax.Node) hcl.Diagnostics {
switch n := node.(type) {
case *hclsyntax.Block:
if r, ok := blockFoldingRange(n, b.InnerBraceRangesForFolding); ok {
ranges = append(ranges, r)
}
case *hclsyntax.ObjectConsExpr:
if r, ok := objectFoldingRange(n, b.InnerBraceRangesForFolding); ok {
ranges = append(ranges, r)
}
}
return nil
})
return ranges
}
// blockFoldingRange returns a folding range for a block.
// The range starts right after the '{' and ends at the '}'.
// This satisfies lsp4ij's requirement that charAt(start-1) == '{' and charAt(end) == '}'.
func blockFoldingRange(block *hclsyntax.Block, innerBraces bool) (Range, bool) {
if !isMultiline(block.Range()) {
return Range{}, false
}
// Body range starts AT '{', we need position AFTER '{'
bodyRange := block.Body.Range()
// Block range ends after '}' - we need to position AT the '}'
blockEnd := block.Range().End
startCol := bodyRange.Start.Column
endCol := bodyRange.End.Column
if innerBraces {
startCol++
endCol--
}
return Range{
StartLine: bodyRange.Start.Line,
StartColumn: startCol,
EndLine: blockEnd.Line,
EndColumn: endCol,
Kind: "region",
}, true
}
// objectFoldingRange returns a folding range for an object expression.
// The range starts right after the '{' and ends at the '}'.
func objectFoldingRange(obj *hclsyntax.ObjectConsExpr, innerBraces bool) (Range, bool) {
r := obj.Range()
if !isMultiline(r) {
return Range{}, false
}
startCol := r.Start.Column
endCol := r.End.Column
if innerBraces {
startCol++
endCol--
}
// Object range starts at '{', we need to start after it
// Object range ends after '}', we need to position at the '}'
return Range{
StartLine: r.Start.Line,
StartColumn: startCol,
EndLine: r.End.Line,
EndColumn: endCol,
Kind: "region",
}, true
}
func isMultiline(r hcl.Range) bool {
return r.End.Line > r.Start.Line
}
package semtok
import (
"fmt"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/funchcl/decoder"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/lang/semtok"
)
func TokensFor(ctx decoder.Context, filename string) ([]semtok.SemanticToken, error) {
file, ok := ctx.HCLFileByName(filename)
if !ok {
return nil, fmt.Errorf("file %s not found", filename)
}
b, ok := ctx.FileBytesByName(filename)
if !ok {
return nil, fmt.Errorf("file %s not found", filename)
}
w := newWalker(file, b)
return w.fileTokens(), nil
}
package semtok
import (
"log"
"reflect"
"sort"
"strings"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/lang/semtok"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/zclconf/go-cty/cty"
)
type writer struct {
stream []semtok.SemanticToken
}
func (w *writer) write(toks ...semtok.SemanticToken) {
w.stream = append(w.stream, toks...)
}
func (w *writer) tokens() []semtok.SemanticToken {
sort.Slice(w.stream, func(i, j int) bool {
lr := w.stream[i].Range.Start
rr := w.stream[j].Range.Start
if lr.Line != rr.Line {
return lr.Line < rr.Line
}
return lr.Column < rr.Column
})
return w.stream
}
type walker struct {
w writer
f *hcl.File
b []byte
}
func newWalker(file *hcl.File, fileBytes []byte) *walker {
return &walker{
w: writer{},
f: file,
b: fileBytes,
}
}
func (w *walker) write(toks ...semtok.SemanticToken) {
w.w.write(toks...)
}
func (w *walker) fileTokens() []semtok.SemanticToken {
body := w.f.Body.(*hclsyntax.Body)
w.body(body, "")
return w.w.tokens()
}
func (w *walker) body(body *hclsyntax.Body, parentBlockType string) {
w.attribute(parentBlockType, body.Attributes)
for _, block := range body.Blocks {
w.block(block)
}
}
func (w *walker) block(block *hclsyntax.Block) {
bt := block.Type
tt := semtok.TokenTypeKeyword
w.write(makeToken(tt, block.TypeRange))
for i := range block.Labels {
switch i {
case 0:
decl := false
tt = semtok.TokenTypeVariable
switch bt {
case "resource", "resources":
tt = semtok.TokenTypeClass
decl = true
case "composite":
tt = semtok.TokenTypeEnumMember
}
tok := makeToken(tt, block.LabelRanges[i])
if decl {
tok = withModifiers(tok, semtok.TokenModifierDefinition)
}
w.write(tok)
}
}
w.body(block.Body, bt)
}
func (w *walker) attribute(blockType string, attributes hclsyntax.Attributes) {
for _, attr := range attributes {
if blockType != "locals" {
w.write(makeToken(semtok.TokenTypeKeyword, attr.NameRange))
} else {
w.write(withModifiers(makeToken(semtok.TokenTypeVariable, attr.NameRange), semtok.TokenModifierDefinition))
}
w.expression(attr.Expr)
}
}
func makeToken(tt semtok.TokenType, r hcl.Range) semtok.SemanticToken {
return semtok.SemanticToken{
Type: tt,
Range: r,
}
}
func withModifiers(tok semtok.SemanticToken, mods ...semtok.TokenModifier) semtok.SemanticToken {
tok.Modifiers = append(tok.Modifiers, mods...)
return tok
}
func (w *walker) processScopeTraversal(exp *hclsyntax.ScopeTraversalExpr) bool {
sourceCode := string(exp.Range().SliceBytes(w.b))
if strings.Contains(sourceCode, "[") {
return false
}
if strings.Contains(sourceCode, "*") {
return false
}
if exp.Range().Start.Line != exp.Range().End.Line {
return false
}
parts := strings.Split(sourceCode, ".")
startPos := exp.Range().Start
startByte := exp.Range().Start.Byte
fileName := exp.Range().Filename
makeRange := func(n int) hcl.Range {
offset := 0
for i := 0; i < n; i++ {
offset += len(parts[i]) + 1 // including next dot
}
return hcl.Range{
Filename: fileName,
Start: hcl.Pos{
Line: startPos.Line,
Column: startPos.Column + offset,
Byte: startByte + offset,
},
End: hcl.Pos{
Line: startPos.Line,
Column: startPos.Column + offset + len(parts[n]),
Byte: startByte + offset + len(parts[n]),
},
}
}
for i, p := range parts {
switch i {
case 0:
switch p {
case "req", "each", "self":
w.write(makeToken(semtok.TokenTypeKeyword, makeRange(i)))
default:
w.write(makeToken(semtok.TokenTypeVariable, makeRange(i)))
}
case 1:
switch p {
case "composite", "composite_connection", "context", "extra_resources":
if parts[0] == "req" {
w.write(makeToken(semtok.TokenTypeKeyword, makeRange(i)))
} else {
w.write(makeToken(semtok.TokenTypeProperty, makeRange(i)))
}
case "resource", "connection", "resources", "connections":
if parts[0] == "req" || parts[0] == "self" {
w.write(makeToken(semtok.TokenTypeKeyword, makeRange(i)))
} else {
w.write(makeToken(semtok.TokenTypeProperty, makeRange(i)))
}
case "name", "basename":
if parts[0] == "self" {
w.write(makeToken(semtok.TokenTypeKeyword, makeRange(i)))
} else {
w.write(makeToken(semtok.TokenTypeProperty, makeRange(i)))
}
}
default:
w.write(makeToken(semtok.TokenTypeProperty, makeRange(i)))
}
}
return true
}
func (w *walker) expression(node hclsyntax.Expression) {
if node == nil {
return
}
switch node := node.(type) {
case *hclsyntax.LiteralValueExpr:
vt := node.Val.Type()
switch vt {
case cty.String:
w.write(makeToken(semtok.TokenTypeString, node.Range()))
case cty.Number:
w.write(makeToken(semtok.TokenTypeNumber, node.Range()))
case cty.Bool:
w.write(makeToken(semtok.TokenTypeKeyword, node.Range()))
default:
// nop
}
case *hclsyntax.FunctionCallExpr:
w.write(makeToken(semtok.TokenTypeFunction, node.NameRange))
for _, arg := range node.Args {
w.expression(arg)
}
case *hclsyntax.ForExpr:
w.write(makeToken(semtok.TokenTypeKeyword, node.OpenRange))
// do something about `in`, `if`, key name, value name etc.
w.expression(node.KeyExpr)
w.expression(node.CollExpr)
w.expression(node.ValExpr)
w.expression(node.CondExpr)
case *hclsyntax.ObjectConsExpr:
for _, item := range node.Items {
w.expression(item.KeyExpr)
w.expression(item.ValueExpr)
}
case *hclsyntax.ScopeTraversalExpr:
if !w.processScopeTraversal(node) {
w.write(makeToken(semtok.TokenTypeVariable, node.Range()))
}
case *hclsyntax.ObjectConsKeyExpr:
switch node.Wrapped.(type) {
case *hclsyntax.ScopeTraversalExpr:
w.write(makeToken(semtok.TokenTypeProperty, node.Wrapped.Range()))
default:
w.expression(node.Wrapped)
}
case *hclsyntax.ConditionalExpr:
w.expression(node.Condition)
//w.write(makeToken(semtok.TokenTypeOperator, hcl.RangeBetween(node.Condition.Range(), node.TrueResult.Range())))
w.expression(node.TrueResult)
//w.write(makeToken(semtok.TokenTypeOperator, hcl.RangeBetween(node.TrueResult.Range(), node.FalseResult.Range())))
w.expression(node.FalseResult)
case *hclsyntax.BinaryOpExpr:
w.expression(node.LHS)
w.write(makeToken(semtok.TokenTypeOperator, hcl.RangeBetween(node.LHS.Range(), node.RHS.Range())))
w.expression(node.RHS)
case *hclsyntax.UnaryOpExpr:
w.write(makeToken(semtok.TokenTypeOperator, node.SymbolRange))
w.expression(node.Val)
case *hclsyntax.TupleConsExpr:
for _, e := range node.Exprs {
w.expression(e)
}
case *hclsyntax.TemplateExpr:
for _, e := range node.Parts {
w.expression(e)
}
case *hclsyntax.TemplateWrapExpr:
w.expression(node.Wrapped)
case *hclsyntax.IndexExpr:
w.expression(node.Collection)
w.expression(node.Key)
case *hclsyntax.SplatExpr:
// FIXME
case *hclsyntax.ParenthesesExpr:
w.expression(node.Expression)
case *hclsyntax.AnonSymbolExpr:
w.write(makeToken(semtok.TokenTypeVariable, node.Range()))
// noop
case *hclsyntax.ExprSyntaxError:
// noop
case *hclsyntax.RelativeTraversalExpr:
w.expression(node.Source)
default:
log.Println("[warn] unknown expression type:", reflect.TypeOf(node))
}
}
// Package symbols provides document symbols.
package symbols
import (
"fmt"
"strings"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/funchcl/decoder"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/lang"
"github.com/hashicorp/hcl/v2"
)
type symbolImplSigil struct{}
// Symbol represents any attribute, or block (and its nested blocks or attributes)
type Symbol interface {
Path() lang.Path
Name() string
NestedSymbols() []Symbol
Range() hcl.Range
isSymbolImpl() symbolImplSigil
}
// Collector collects symbols from documents.
type Collector struct {
path lang.Path
}
// NewCollector returns a collector.
func NewCollector(path lang.Path) *Collector {
return &Collector{path: path}
}
// FileSymbols find all symbols in a file as a hierarchical structure.
func (c *Collector) FileSymbols(ctx decoder.Context, filename string) ([]Symbol, error) {
file, ok := ctx.HCLFileByName(filename)
if !ok {
return nil, fmt.Errorf("file %s not found", filename)
}
return c.symbolsForBody(file.Body), nil
}
// WorkspaceSymbols finds all symbols in all modules that are currently open.
// Note that this is buggy in that it doesn't load symbols from modules that are not
// being edited.
func WorkspaceSymbols(provider decoder.ContextProvider, query string) ([]Symbol, error) {
var ret []Symbol
paths, err := provider.Paths()
if err != nil {
return nil, err
}
for _, p := range paths {
pc, err := provider.PathContext(p)
if err != nil {
return nil, err
}
for _, file := range pc.Files() {
syms, err := NewCollector(p).FileSymbols(pc, file)
if err != nil {
return nil, err
}
for _, sym := range syms {
if strings.Contains(sym.Name(), query) {
ret = append(ret, sym)
}
}
}
}
return ret, nil
}
package symbols
import (
"fmt"
"sort"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/funchcl/decoder/decoderutils"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/lang"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/zclconf/go-cty/cty"
)
func (c *Collector) symbolsForBody(body hcl.Body) []Symbol {
var symbols []Symbol
if body == nil {
return symbols
}
content := body.(*hclsyntax.Body)
for name, attr := range content.Attributes {
symbols = append(symbols, &AttributeSymbol{
AttrName: name,
ExprKind: symbolExprKind(attr.Expr),
path: c.path,
rng: attr.Range(),
nestedSymbols: c.nestedSymbolsForExpr(attr.Expr),
})
}
for _, block := range content.Blocks {
symbols = append(symbols, &BlockSymbol{
Type: block.Type,
Labels: block.Labels,
path: c.path,
rng: block.Range(),
nestedSymbols: c.symbolsForBody(block.Body),
})
}
sort.SliceStable(symbols, func(i, j int) bool {
return symbols[i].Range().Start.Byte < symbols[j].Range().Start.Byte
})
return symbols
}
func symbolExprKind(expr hcl.Expression) lang.SymbolExprKind {
switch e := expr.(type) {
case *hclsyntax.ScopeTraversalExpr:
return lang.ReferenceExprKind{}
case *hclsyntax.LiteralValueExpr:
return lang.LiteralTypeKind{Type: e.Val.Type()}
case *hclsyntax.TemplateExpr:
if e.IsStringLiteral() {
return lang.LiteralTypeKind{Type: cty.String}
}
if decoderutils.IsMultilineStringLiteral(e) {
return lang.LiteralTypeKind{Type: cty.String}
}
case *hclsyntax.TupleConsExpr:
return lang.TupleConsExprKind{}
case *hclsyntax.ObjectConsExpr:
return lang.ObjectConsExprKind{}
default:
}
return nil
}
func (c *Collector) nestedSymbolsForExpr(expr hcl.Expression) []Symbol {
var symbols []Symbol
switch e := expr.(type) {
case *hclsyntax.TupleConsExpr:
for i, item := range e.ExprList() {
symbols = append(symbols, &ExprSymbol{
ExprName: fmt.Sprintf("%d", i),
ExprKind: symbolExprKind(item),
path: c.path,
rng: item.Range(),
nestedSymbols: c.nestedSymbolsForExpr(item),
})
}
case *hclsyntax.ObjectConsExpr:
for _, item := range e.Items {
key, _ := item.KeyExpr.Value(nil)
if key.IsNull() || !key.IsWhollyKnown() || key.Type() != cty.String {
// skip items keys that can't be interpolated
// without further context
continue
}
symbols = append(symbols, &ExprSymbol{
ExprName: key.AsString(),
ExprKind: symbolExprKind(item.ValueExpr),
path: c.path,
rng: hcl.RangeBetween(item.KeyExpr.Range(), item.ValueExpr.Range()),
nestedSymbols: c.nestedSymbolsForExpr(item.ValueExpr),
})
}
}
return symbols
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package symbols
import (
"fmt"
"reflect"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/lang"
"github.com/hashicorp/hcl/v2"
)
// BlockSymbol is Symbol implementation representing a block
type BlockSymbol struct {
Type string
Labels []string
path lang.Path
rng hcl.Range
nestedSymbols []Symbol
}
func (*BlockSymbol) isSymbolImpl() symbolImplSigil {
return symbolImplSigil{}
}
func (bs *BlockSymbol) Equal(other Symbol) bool {
obs, ok := other.(*BlockSymbol)
if !ok {
return false
}
if bs == nil || obs == nil {
return bs == obs
}
return reflect.DeepEqual(*bs, *obs)
}
func (bs *BlockSymbol) Name() string {
name := bs.Type
for _, label := range bs.Labels {
name += fmt.Sprintf(" %q", label)
}
return name
}
func (bs *BlockSymbol) NestedSymbols() []Symbol {
return bs.nestedSymbols
}
func (bs *BlockSymbol) Range() hcl.Range {
return bs.rng
}
func (bs *BlockSymbol) Path() lang.Path {
return bs.path
}
// AttributeSymbol is Symbol implementation representing an attribute
type AttributeSymbol struct {
AttrName string
ExprKind lang.SymbolExprKind
path lang.Path
rng hcl.Range
nestedSymbols []Symbol
}
func (*AttributeSymbol) isSymbolImpl() symbolImplSigil {
return symbolImplSigil{}
}
func (as *AttributeSymbol) Equal(other Symbol) bool {
oas, ok := other.(*AttributeSymbol)
if !ok {
return false
}
if as == nil || oas == nil {
return as == oas
}
return reflect.DeepEqual(*as, *oas)
}
func (as *AttributeSymbol) Name() string {
return as.AttrName
}
func (as *AttributeSymbol) NestedSymbols() []Symbol {
return as.nestedSymbols
}
func (as *AttributeSymbol) Range() hcl.Range {
return as.rng
}
func (as *AttributeSymbol) Path() lang.Path {
return as.path
}
type ExprSymbol struct {
ExprName string
ExprKind lang.SymbolExprKind
path lang.Path
rng hcl.Range
nestedSymbols []Symbol
}
func (*ExprSymbol) isSymbolImpl() symbolImplSigil {
return symbolImplSigil{}
}
func (as *ExprSymbol) Equal(other Symbol) bool {
oas, ok := other.(*ExprSymbol)
if !ok {
return false
}
if as == nil || oas == nil {
return as == oas
}
return reflect.DeepEqual(*as, *oas)
}
func (as *ExprSymbol) Name() string {
return as.ExprName
}
func (as *ExprSymbol) NestedSymbols() []Symbol {
return as.nestedSymbols
}
func (as *ExprSymbol) Range() hcl.Range {
return as.rng
}
func (as *ExprSymbol) Path() lang.Path {
return as.path
}
// Package schema provides the standard schema for the function-hcl DSL.
package schema
import (
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/schema"
"github.com/hashicorp/hcl/v2/hclsyntax"
)
const LanguageHCL = "hcl"
// DynamicLookup provides a schema for the supplied API version and kind.
type DynamicLookup interface {
Schema(apiVersion, kind string) *schema.AttributeSchema
}
// LocalsAttributeLookup is an optional interface that can be implemented by DynamicLookup
// to dynamically figure out schemas for local variables based on how they are assigned.
// (for example: foo = req.composite.metadata.name => foo is of type string)
type LocalsAttributeLookup interface {
LocalSchema(name string) *schema.AttributeSchema
}
// CompositeSchemaLookup is an optional interface that can be implemented by DynamicLookup
// to dynamically figure out the schema for the composite.
type CompositeSchemaLookup interface {
CompositeSchema() *schema.AttributeSchema
}
// New returns a schema.Lookup instance.
func New(dyn DynamicLookup) schema.Lookup {
return &lookup{dyn: dyn}
}
// BasicK8sObjectConstraint returns a constraint for a generic K8s object.
func BasicK8sObjectConstraint() schema.Object {
return basicK8sObjectSchema()
}
// DependentSchemaOrDefault returns an available schema for a body attribute or a default.
func DependentSchemaOrDefault(dyn DynamicLookup, bodyBlock *hclsyntax.Block) *schema.AttributeSchema {
ret, ok := dependentSchema(dyn, bodyBlock)
if !ok {
return basicBodyAttributeSchema()
}
return ret
}
func withAttributes(cons schema.Object, attrs map[string]*schema.AttributeSchema) schema.Object {
return schema.Object{
Name: cons.Name,
Attributes: attrs,
Description: cons.Description,
AllowInterpolatedKeys: cons.AllowInterpolatedKeys,
AnyAttribute: cons.AnyAttribute,
}
}
// WithoutStatus returns an attribute schema that eliminates the `status` property
// if one is present.
func WithoutStatus(aSchema *schema.AttributeSchema) *schema.AttributeSchema {
if aSchema == nil {
return nil
}
cons, ok := aSchema.Constraint.(schema.Object)
if !ok {
return aSchema
}
_, hasStatus := cons.Attributes["status"]
if !hasStatus {
return aSchema
}
aSchema = aSchema.Copy()
attrs := make(map[string]*schema.AttributeSchema, len(cons.Attributes))
for k, v := range cons.Attributes {
if k == "status" {
continue
}
attrs[k] = v
}
aSchema.Constraint = withAttributes(cons, attrs)
return aSchema
}
// WithStatusOnly returns an attribute schema that only has the `status` property
// if one is present. Otherwise, the schema is returned as-is.
func WithStatusOnly(aSchema *schema.AttributeSchema) *schema.AttributeSchema {
if aSchema == nil {
return nil
}
cons, ok := aSchema.Constraint.(schema.Object)
if !ok {
return aSchema
}
_, hasStatus := cons.Attributes["status"]
if !hasStatus {
return aSchema
}
aSchema = aSchema.Copy()
aSchema.Constraint = withAttributes(cons, map[string]*schema.AttributeSchema{
"status": cons.Attributes["status"],
})
return aSchema
}
// WithoutAPIVersionAndKind returns an attribute schema that eliminates the `apiVersion`
// and `kind` properties, if present.
func WithoutAPIVersionAndKind(aSchema *schema.AttributeSchema) *schema.AttributeSchema {
if aSchema == nil {
return nil
}
cons, ok := aSchema.Constraint.(schema.Object)
if !ok {
return aSchema
}
aSchema = aSchema.Copy()
attrs := make(map[string]*schema.AttributeSchema, len(cons.Attributes))
for k, v := range cons.Attributes {
if k == "apiVersion" || k == "kind" {
continue
}
attrs[k] = v
}
aSchema.Constraint = withAttributes(cons, attrs)
return aSchema
}
package schema
import (
"log"
"sort"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/schema"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/function"
)
// TODO: add signature for "invoke"
var StandardFunctions = stdFunctions()
func stdFunctions() map[string]schema.FunctionSignature {
return map[string]schema.FunctionSignature{
"abs": {
Params: []function.Parameter{
{
Name: "num",
Type: cty.Number,
},
},
ReturnType: cty.Number,
Description: "`abs` returns the absolute value of the given number. In other words, if the number is zero or positive then it is returned as-is, but if it is negative then it is multiplied by -1 to make it positive before returning it.",
},
"alltrue": {
Params: []function.Parameter{
{
Name: "list",
Type: cty.List(cty.Bool),
},
},
ReturnType: cty.Bool,
Description: "`alltrue` returns `true` if all elements in a given collection are `true` or `\"true\"`. It also returns `true` if the collection is empty.",
},
"anytrue": {
Params: []function.Parameter{
{
Name: "list",
Type: cty.List(cty.Bool),
},
},
ReturnType: cty.Bool,
Description: "`anytrue` returns `true` if any element in a given collection is `true` or `\"true\"`. It also returns `false` if the collection is empty.",
},
"base64decode": {
Params: []function.Parameter{
{
Name: "str",
Type: cty.String,
},
},
ReturnType: cty.String,
Description: "`base64decode` takes a string containing a Base64 character sequence and returns the original string.",
},
"base64encode": {
Params: []function.Parameter{
{
Name: "str",
Type: cty.String,
},
},
ReturnType: cty.String,
Description: "`base64encode` applies Base64 encoding to a string.",
},
"base64gzip": {
Params: []function.Parameter{
{
Name: "str",
Type: cty.String,
},
},
ReturnType: cty.String,
Description: "`base64gzip` compresses a string with gzip and then encodes the result in Base64 encoding.",
},
"base64sha256": {
Params: []function.Parameter{
{
Name: "str",
Type: cty.String,
},
},
ReturnType: cty.String,
Description: "`base64sha256` computes the SHA256 hash of a given string and encodes it with Base64. This is not equivalent to `base64encode(sha256(\"test\"))` since `sha256()` returns hexadecimal representation.",
},
"base64sha512": {
Params: []function.Parameter{
{
Name: "str",
Type: cty.String,
},
},
ReturnType: cty.String,
Description: "`base64sha512` computes the SHA512 hash of a given string and encodes it with Base64. This is not equivalent to `base64encode(sha512(\"test\"))` since `sha512()` returns hexadecimal representation.",
},
"can": {
Params: []function.Parameter{
{
Name: "expression",
Type: cty.DynamicPseudoType,
},
},
ReturnType: cty.Bool,
Description: "`can` evaluates the given expression and returns a boolean value indicating whether the expression produced a result without any errors.",
},
"ceil": {
Params: []function.Parameter{
{
Name: "num",
Type: cty.Number,
},
},
ReturnType: cty.Number,
Description: "`ceil` returns the closest whole number that is greater than or equal to the given value, which may be a fraction.",
},
"chomp": {
Params: []function.Parameter{
{
Name: "str",
Type: cty.String,
},
},
ReturnType: cty.String,
Description: "`chomp` removes newline characters at the end of a string.",
},
"chunklist": {
Params: []function.Parameter{
{
Name: "list",
Type: cty.List(cty.DynamicPseudoType),
},
{
Name: "size",
Description: "The maximum length of each chunk. All but the last element of the result is guaranteed to be of exactly this size.",
Type: cty.Number,
},
},
ReturnType: cty.List(cty.List(cty.DynamicPseudoType)),
Description: "`chunklist` splits a single list into fixed-size chunks, returning a list of lists.",
},
"cidrhost": {
Params: []function.Parameter{
{
Name: "prefix",
Description: "`prefix` must be given in CIDR notation, as defined in [RFC 4632 section 3.1](https://tools.ietf.org/html/rfc4632#section-3.1).",
Type: cty.String,
},
{
Name: "hostnum",
Description: "`hostnum` is a whole number that can be represented as a binary integer with no more than the number of digits remaining in the address after the given prefix.",
Type: cty.Number,
},
},
ReturnType: cty.String,
Description: "`cidrhost` calculates a full host IP address for a given host number within a given IP network address prefix.",
},
"cidrnetmask": {
Params: []function.Parameter{
{
Name: "prefix",
Description: "`prefix` must be given in CIDR notation, as defined in [RFC 4632 section 3.1](https://tools.ietf.org/html/rfc4632#section-3.1).",
Type: cty.String,
},
},
ReturnType: cty.String,
Description: "`cidrnetmask` converts an IPv4 address prefix given in CIDR notation into a subnet mask address.",
},
"cidrsubnet": {
Params: []function.Parameter{
{
Name: "prefix",
Description: "`prefix` must be given in CIDR notation, as defined in [RFC 4632 section 3.1](https://tools.ietf.org/html/rfc4632#section-3.1).",
Type: cty.String,
},
{
Name: "newbits",
Description: "`newbits` is the number of additional bits with which to extend the prefix.",
Type: cty.Number,
},
{
Name: "netnum",
Description: "`netnum` is a whole number that can be represented as a binary integer with no more than `newbits` binary digits, which will be used to populate the additional bits added to the prefix.",
Type: cty.Number,
},
},
ReturnType: cty.String,
Description: "`cidrsubnet` calculates a subnet address within given IP network address prefix.",
},
"cidrsubnets": {
Params: []function.Parameter{
{
Name: "prefix",
Description: "`prefix` must be given in CIDR notation, as defined in [RFC 4632 section 3.1](https://tools.ietf.org/html/rfc4632#section-3.1).",
Type: cty.String,
},
},
VarParam: &function.Parameter{
Name: "newbits",
Description: "",
Type: cty.Number,
},
ReturnType: cty.List(cty.String),
Description: "`cidrsubnets` calculates a sequence of consecutive IP address ranges within a particular CIDR prefix.",
},
"coalesce": {
VarParam: &function.Parameter{
Name: "vals",
Description: "",
Type: cty.DynamicPseudoType,
},
ReturnType: cty.DynamicPseudoType,
Description: "`coalesce` takes any number of arguments and returns the first one that isn't null or an empty string.",
},
"coalescelist": {
VarParam: &function.Parameter{
Name: "vals",
Description: "List or tuple values to test in the given order.",
Type: cty.DynamicPseudoType,
},
ReturnType: cty.List(cty.DynamicPseudoType),
Description: "`coalescelist` takes any number of list arguments and returns the first one that isn't empty.",
},
"compact": {
Params: []function.Parameter{
{
Name: "list",
Type: cty.List(cty.String),
},
},
ReturnType: cty.List(cty.String),
Description: "`compact` takes a list of strings and returns a new list with any empty string elements removed.",
},
"concat": {
VarParam: &function.Parameter{
Name: "seqs",
Description: "",
Type: cty.DynamicPseudoType,
},
ReturnType: cty.List(cty.DynamicPseudoType),
Description: "`concat` takes two or more lists and combines them into a single list.",
},
"contains": {
Params: []function.Parameter{
{
Name: "list",
Type: cty.DynamicPseudoType,
},
{
Name: "value",
Type: cty.DynamicPseudoType,
},
},
ReturnType: cty.Bool,
Description: "`contains` determines whether a given list or set contains a given single value as one of its elements.",
},
"csvdecode": {
Params: []function.Parameter{
{
Name: "str",
Type: cty.String,
},
},
ReturnType: cty.List(cty.DynamicPseudoType),
Description: "`csvdecode` decodes a string containing CSV-formatted data and produces a list of maps representing that data.",
},
"distinct": {
Params: []function.Parameter{
{
Name: "list",
Type: cty.List(cty.DynamicPseudoType),
},
},
ReturnType: cty.List(cty.DynamicPseudoType),
Description: "`distinct` takes a list and returns a new list with any duplicate elements removed.",
},
"element": {
Params: []function.Parameter{
{
Name: "list",
Type: cty.DynamicPseudoType,
},
{
Name: "index",
Type: cty.Number,
},
},
ReturnType: cty.DynamicPseudoType,
Description: "`element` retrieves a single element from a list.",
},
"endswith": {
Params: []function.Parameter{
{
Name: "str",
Type: cty.String,
},
{
Name: "suffix",
Type: cty.String,
},
},
ReturnType: cty.Bool,
Description: "`endswith` takes two values: a string to check and a suffix string. The function returns true if the first string ends with that exact suffix.",
},
"flatten": {
Params: []function.Parameter{
{
Name: "list",
Type: cty.DynamicPseudoType,
},
},
ReturnType: cty.List(cty.DynamicPseudoType),
Description: "`flatten` takes a list and replaces any elements that are lists with a flattened sequence of the list contents.",
},
"floor": {
Params: []function.Parameter{
{
Name: "num",
Type: cty.Number,
},
},
ReturnType: cty.Number,
Description: "`floor` returns the closest whole number that is less than or equal to the given value, which may be a fraction.",
},
"format": {
Params: []function.Parameter{
{
Name: "format",
Type: cty.String,
},
},
VarParam: &function.Parameter{
Name: "args",
Description: "",
Type: cty.DynamicPseudoType,
},
ReturnType: cty.String,
Description: "The `format` function produces a string by formatting a number of other values according to a specification string. It is similar to the `printf` function in C, and other similar functions in other programming languages.",
},
"formatdate": {
Params: []function.Parameter{
{
Name: "format",
Type: cty.String,
},
{
Name: "time",
Type: cty.String,
},
},
ReturnType: cty.String,
Description: "`formatdate` converts a timestamp into a different time format.",
},
"formatlist": {
Params: []function.Parameter{
{
Name: "format",
Type: cty.String,
},
},
VarParam: &function.Parameter{
Name: "args",
Description: "",
Type: cty.DynamicPseudoType,
},
ReturnType: cty.List(cty.String),
Description: "`formatlist` produces a list of strings by formatting a number of other values according to a specification string.",
},
"indent": {
Params: []function.Parameter{
{
Name: "spaces",
Description: "Number of spaces to add after each newline character.",
Type: cty.Number,
},
{
Name: "str",
Type: cty.String,
},
},
ReturnType: cty.String,
Description: "`indent` adds a given number of spaces to the beginnings of all but the first line in a given multi-line string.",
},
"index": {
Params: []function.Parameter{
{
Name: "list",
Type: cty.DynamicPseudoType,
},
{
Name: "value",
Type: cty.DynamicPseudoType,
},
},
ReturnType: cty.Number,
Description: "`index` finds the element index for a given value in a list.",
},
"join": {
Params: []function.Parameter{
{
Name: "separator",
Description: "Delimiter to insert between the given strings.",
Type: cty.String,
},
},
VarParam: &function.Parameter{
Name: "lists",
Description: "One or more lists of strings to join.",
Type: cty.List(cty.String),
},
ReturnType: cty.String,
Description: "`join` produces a string by concatenating together all elements of a given list of strings with the given delimiter.",
},
"jsondecode": {
Params: []function.Parameter{
{
Name: "str",
Type: cty.String,
},
},
ReturnType: cty.DynamicPseudoType,
Description: "`jsondecode` interprets a given string as JSON, returning a representation of the result of decoding that string.",
},
"jsonencode": {
Params: []function.Parameter{
{
Name: "val",
Type: cty.DynamicPseudoType,
},
},
ReturnType: cty.String,
Description: "`jsonencode` encodes a given value to a string using JSON syntax.",
},
"keys": {
Params: []function.Parameter{
{
Name: "inputMap",
Description: "The map to extract keys from. May instead be an object-typed value, in which case the result is a tuple of the object attributes.",
Type: cty.DynamicPseudoType,
},
},
// ReturnType: cty.DynamicPseudoType,
ReturnType: cty.List(cty.String),
Description: "`keys` takes a map and returns a list containing the keys from that map.",
},
"length": {
Params: []function.Parameter{
{
Name: "value",
Type: cty.DynamicPseudoType,
},
},
ReturnType: cty.Number,
Description: "`length` determines the length of a given list, map, or string.",
},
"log": {
Params: []function.Parameter{
{
Name: "num",
Type: cty.Number,
},
{
Name: "base",
Type: cty.Number,
},
},
ReturnType: cty.Number,
Description: "`log` returns the logarithm of a given number in a given base.",
},
"lookup": {
Params: []function.Parameter{
{
Name: "inputMap",
Type: cty.DynamicPseudoType,
},
{
Name: "key",
Type: cty.String,
},
},
VarParam: &function.Parameter{
Name: "default",
Description: "",
Type: cty.DynamicPseudoType,
},
ReturnType: cty.DynamicPseudoType,
Description: "`lookup` retrieves the value of a single element from a map, given its key. If the given key does not exist, the given default value is returned instead.",
},
"lower": {
Params: []function.Parameter{
{
Name: "str",
Type: cty.String,
},
},
ReturnType: cty.String,
Description: "`lower` converts all cased letters in the given string to lowercase.",
},
"matchkeys": {
Params: []function.Parameter{
{
Name: "values",
Type: cty.List(cty.DynamicPseudoType),
},
{
Name: "keys",
Type: cty.List(cty.DynamicPseudoType),
},
{
Name: "searchset",
Type: cty.List(cty.DynamicPseudoType),
},
},
ReturnType: cty.List(cty.DynamicPseudoType),
Description: "`matchkeys` constructs a new list by taking a subset of elements from one list whose indexes match the corresponding indexes of values in another list.",
},
"max": {
VarParam: &function.Parameter{
Name: "numbers",
Description: "",
Type: cty.Number,
},
ReturnType: cty.Number,
Description: "`max` takes one or more numbers and returns the greatest number from the set.",
},
"md5": {
Params: []function.Parameter{
{
Name: "str",
Type: cty.String,
},
},
ReturnType: cty.String,
Description: "`md5` computes the MD5 hash of a given string and encodes it with hexadecimal digits.",
},
"merge": {
VarParam: &function.Parameter{
Name: "maps",
Description: "",
Type: cty.DynamicPseudoType,
},
ReturnType: cty.DynamicPseudoType,
Description: "`merge` takes an arbitrary number of maps or objects, and returns a single map or object that contains a merged set of elements from all arguments.",
},
"min": {
VarParam: &function.Parameter{
Name: "numbers",
Description: "",
Type: cty.Number,
},
ReturnType: cty.Number,
Description: "`min` takes one or more numbers and returns the smallest number from the set.",
},
"one": {
Params: []function.Parameter{
{
Name: "list",
Type: cty.DynamicPseudoType,
},
},
ReturnType: cty.DynamicPseudoType,
Description: "`one` takes a list, set, or tuple value with either zero or one elements. If the collection is empty, `one` returns `null`. Otherwise, `one` returns the first element. If there are two or more elements then `one` will return an error.",
},
"parseint": {
Params: []function.Parameter{
{
Name: "number",
Type: cty.DynamicPseudoType,
},
{
Name: "base",
Type: cty.Number,
},
},
ReturnType: cty.Number,
Description: "`parseint` parses the given string as a representation of an integer in the specified base and returns the resulting number. The base must be between 2 and 62 inclusive.",
},
"pow": {
Params: []function.Parameter{
{
Name: "num",
Type: cty.Number,
},
{
Name: "power",
Type: cty.Number,
},
},
ReturnType: cty.Number,
Description: "`pow` calculates an exponent, by raising its first argument to the power of the second argument.",
},
"range": {
VarParam: &function.Parameter{
Name: "params",
Description: "",
Type: cty.Number,
},
ReturnType: cty.List(cty.Number),
Description: "`range` generates a list of numbers using a start value, a limit value, and a step value.",
},
"regex": {
Params: []function.Parameter{
{
Name: "pattern",
Type: cty.String,
},
{
Name: "string",
Type: cty.String,
},
},
ReturnType: cty.List(cty.String),
Description: "`regex` applies a [regular expression](https://en.wikipedia.org/wiki/Regular_expression) to a string and returns the matching substrings.",
},
"regexall": {
Params: []function.Parameter{
{
Name: "pattern",
Type: cty.String,
},
{
Name: "string",
Type: cty.String,
},
},
ReturnType: cty.List(cty.String),
Description: "`regexall` applies a [regular expression](https://en.wikipedia.org/wiki/Regular_expression) to a string and returns a list of all matches.",
},
"replace": {
Params: []function.Parameter{
{
Name: "str",
Type: cty.String,
},
{
Name: "substr",
Type: cty.String,
},
{
Name: "replace",
Type: cty.String,
},
},
ReturnType: cty.String,
Description: "`replace` searches a given string for another given substring, and replaces each occurrence with a given replacement string.",
},
"reverse": {
Params: []function.Parameter{
{
Name: "list",
Type: cty.DynamicPseudoType,
},
},
ReturnType: cty.List(cty.DynamicPseudoType),
Description: "`reverse` takes a sequence and produces a new sequence of the same length with all of the same elements as the given sequence but in reverse order.",
},
"rsadecrypt": {
Params: []function.Parameter{
{
Name: "ciphertext",
Type: cty.String,
},
{
Name: "privatekey",
Type: cty.String,
},
},
ReturnType: cty.String,
Description: "`rsadecrypt` decrypts an RSA-encrypted ciphertext, returning the corresponding cleartext.",
},
"setintersection": {
Params: []function.Parameter{
{
Name: "first_set",
Type: cty.Set(cty.DynamicPseudoType),
},
},
VarParam: &function.Parameter{
Name: "other_sets",
Description: "",
Type: cty.Set(cty.DynamicPseudoType),
},
ReturnType: cty.Set(cty.DynamicPseudoType),
Description: "The `setintersection` function takes multiple sets and produces a single set containing only the elements that all of the given sets have in common. In other words, it computes the [intersection](https://en.wikipedia.org/wiki/Intersection_\\(set_theory\\)) of the sets.",
},
"setproduct": {
VarParam: &function.Parameter{
Name: "sets",
Description: "The sets to consider. Also accepts lists and tuples, and if all arguments are of list or tuple type then the result will preserve the input ordering",
Type: cty.DynamicPseudoType,
},
ReturnType: cty.DynamicPseudoType,
Description: "The `setproduct` function finds all of the possible combinations of elements from all of the given sets by computing the [Cartesian product](https://en.wikipedia.org/wiki/Cartesian_product).",
},
"setsubtract": {
Params: []function.Parameter{
{
Name: "a",
Type: cty.Set(cty.DynamicPseudoType),
},
{
Name: "b",
Type: cty.Set(cty.DynamicPseudoType),
},
},
ReturnType: cty.Set(cty.DynamicPseudoType),
Description: "The `setsubtract` function returns a new set containing the elements from the first set that are not present in the second set. In other words, it computes the [relative complement](https://en.wikipedia.org/wiki/Complement_\\(set_theory\\)#Relative_complement) of the second set.",
},
"setunion": {
Params: []function.Parameter{
{
Name: "first_set",
Type: cty.Set(cty.DynamicPseudoType),
},
},
VarParam: &function.Parameter{
Name: "other_sets",
Description: "",
Type: cty.Set(cty.DynamicPseudoType),
},
ReturnType: cty.Set(cty.DynamicPseudoType),
Description: "The `setunion` function takes multiple sets and produces a single set containing the elements from all of the given sets. In other words, it computes the [union](https://en.wikipedia.org/wiki/Union_\\(set_theory\\)) of the sets.",
},
"sha1": {
Params: []function.Parameter{
{
Name: "str",
Type: cty.String,
},
},
ReturnType: cty.String,
Description: "`sha1` computes the SHA1 hash of a given string and encodes it with hexadecimal digits.",
},
"sha256": {
Params: []function.Parameter{
{
Name: "str",
Type: cty.String,
},
},
ReturnType: cty.String,
Description: "`sha256` computes the SHA256 hash of a given string and encodes it with hexadecimal digits.",
},
"sha512": {
Params: []function.Parameter{
{
Name: "str",
Type: cty.String,
},
},
ReturnType: cty.String,
Description: "`sha512` computes the SHA512 hash of a given string and encodes it with hexadecimal digits.",
},
"signum": {
Params: []function.Parameter{
{
Name: "num",
Type: cty.Number,
},
},
ReturnType: cty.Number,
Description: "`signum` determines the sign of a number, returning a number between -1 and 1 to represent the sign.",
},
"slice": {
Params: []function.Parameter{
{
Name: "list",
Type: cty.DynamicPseudoType,
},
{
Name: "start_index",
Type: cty.Number,
},
{
Name: "end_index",
Type: cty.Number,
},
},
ReturnType: cty.List(cty.DynamicPseudoType),
Description: "`slice` extracts some consecutive elements from within a list.",
},
"sort": {
Params: []function.Parameter{
{
Name: "list",
Type: cty.List(cty.String),
},
},
ReturnType: cty.List(cty.String),
Description: "`sort` takes a list of strings and returns a new list with those strings sorted lexicographically.",
},
"split": {
Params: []function.Parameter{
{
Name: "separator",
Type: cty.String,
},
{
Name: "str",
Type: cty.String,
},
},
ReturnType: cty.List(cty.String),
Description: "`split` produces a list by dividing a given string at all occurrences of a given separator.",
},
"startswith": {
Params: []function.Parameter{
{
Name: "str",
Type: cty.String,
},
{
Name: "prefix",
Type: cty.String,
},
},
ReturnType: cty.Bool,
Description: "`startswith` takes two values: a string to check and a prefix string. The function returns true if the string begins with that exact prefix.",
},
"strcontains": {
Params: []function.Parameter{
{
Name: "str",
Type: cty.String,
},
{
Name: "substr",
Type: cty.String,
},
},
ReturnType: cty.Bool,
Description: "`strcontains` takes two values: a string to check and an expected substring. The function returns true if the string has the substring contained within it.",
},
"strrev": {
Params: []function.Parameter{
{
Name: "str",
Type: cty.String,
},
},
ReturnType: cty.String,
Description: "`strrev` reverses the characters in a string. Note that the characters are treated as _Unicode characters_ (in technical terms, Unicode [grapheme cluster boundaries](https://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries) are respected).",
},
"substr": {
Params: []function.Parameter{
{
Name: "str",
Type: cty.String,
},
{
Name: "offset",
Type: cty.Number,
},
{
Name: "length",
Type: cty.Number,
},
},
ReturnType: cty.String,
Description: "`substr` extracts a substring from a given string by offset and (maximum) length.",
},
"sum": {
Params: []function.Parameter{
{
Name: "list",
Type: cty.DynamicPseudoType,
},
},
ReturnType: cty.Number,
Description: "`sum` takes a list or set of numbers and returns the sum of those numbers.",
},
"textdecodebase64": {
Params: []function.Parameter{
{
Name: "source",
Type: cty.String,
},
{
Name: "encoding",
Type: cty.String,
},
},
ReturnType: cty.String,
Description: "`textdecodebase64` function decodes a string that was previously Base64-encoded, and then interprets the result as characters in a specified character encoding.",
},
"textencodebase64": {
Params: []function.Parameter{
{
Name: "string",
Type: cty.String,
},
{
Name: "encoding",
Type: cty.String,
},
},
ReturnType: cty.String,
Description: "`textencodebase64` encodes the unicode characters in a given string using a specified character encoding, returning the result base64 encoded because Terraform language strings are always sequences of unicode characters.",
},
"timeadd": {
Params: []function.Parameter{
{
Name: "timestamp",
Type: cty.String,
},
{
Name: "duration",
Type: cty.String,
},
},
ReturnType: cty.String,
Description: "`timeadd` adds a duration to a timestamp, returning a new timestamp.",
},
"timecmp": {
Params: []function.Parameter{
{
Name: "timestamp_a",
Type: cty.String,
},
{
Name: "timestamp_b",
Type: cty.String,
},
},
ReturnType: cty.Number,
Description: "`timecmp` compares two timestamps and returns a number that represents the ordering of the instants those timestamps represent.",
},
"title": {
Params: []function.Parameter{
{
Name: "str",
Type: cty.String,
},
},
ReturnType: cty.String,
Description: "`title` converts the first letter of each word in the given string to uppercase.",
},
"tobool": {
Params: []function.Parameter{
{
Name: "v",
Type: cty.DynamicPseudoType,
},
},
ReturnType: cty.Bool,
Description: "`tobool` converts its argument to a boolean value.",
},
"tolist": {
Params: []function.Parameter{
{
Name: "v",
Type: cty.DynamicPseudoType,
},
},
ReturnType: cty.List(cty.DynamicPseudoType),
Description: "`tolist` converts its argument to a list value.",
},
"tomap": {
Params: []function.Parameter{
{
Name: "v",
Type: cty.DynamicPseudoType,
},
},
ReturnType: cty.Map(cty.DynamicPseudoType),
Description: "`tomap` converts its argument to a map value.",
},
"tonumber": {
Params: []function.Parameter{
{
Name: "v",
Type: cty.DynamicPseudoType,
},
},
ReturnType: cty.Number,
Description: "`tonumber` converts its argument to a number value.",
},
"toset": {
Params: []function.Parameter{
{
Name: "v",
Type: cty.DynamicPseudoType,
},
},
ReturnType: cty.Set(cty.DynamicPseudoType),
Description: "`toset` converts its argument to a set value.",
},
"tostring": {
Params: []function.Parameter{
{
Name: "v",
Type: cty.DynamicPseudoType,
},
},
ReturnType: cty.String,
Description: "`tostring` converts its argument to a string value.",
},
"transpose": {
Params: []function.Parameter{
{
Name: "values",
Type: cty.Map(cty.List(cty.String)),
},
},
ReturnType: cty.Map(cty.List(cty.String)),
Description: "`transpose` takes a map of lists of strings and swaps the keys and values to produce a new map of lists of strings.",
},
"trim": {
Params: []function.Parameter{
{
Name: "str",
Type: cty.String,
},
{
Name: "cutset",
Description: "A string containing all of the characters to trim. Each character is taken separately, so the order of characters is insignificant.",
Type: cty.String,
},
},
ReturnType: cty.String,
Description: "`trim` removes the specified set of characters from the start and end of the given string.",
},
"trimprefix": {
Params: []function.Parameter{
{
Name: "str",
Type: cty.String,
},
{
Name: "prefix",
Type: cty.String,
},
},
ReturnType: cty.String,
Description: "`trimprefix` removes the specified prefix from the start of the given string. If the string does not start with the prefix, the string is returned unchanged.",
},
"trimspace": {
Params: []function.Parameter{
{
Name: "str",
Type: cty.String,
},
},
ReturnType: cty.String,
Description: "`trimspace` removes any space characters from the start and end of the given string.",
},
"trimsuffix": {
Params: []function.Parameter{
{
Name: "str",
Type: cty.String,
},
{
Name: "suffix",
Type: cty.String,
},
},
ReturnType: cty.String,
Description: "`trimsuffix` removes the specified suffix from the end of the given string.",
},
"try": {
VarParam: &function.Parameter{
Name: "expressions",
Description: "",
Type: cty.DynamicPseudoType,
},
ReturnType: cty.DynamicPseudoType,
Description: "`try` evaluates all of its argument expressions in turn and returns the result of the first one that does not produce any errors.",
},
"upper": {
Params: []function.Parameter{
{
Name: "str",
Type: cty.String,
},
},
ReturnType: cty.String,
Description: "`upper` converts all cased letters in the given string to uppercase.",
},
"urlencode": {
Params: []function.Parameter{
{
Name: "str",
Type: cty.String,
},
},
ReturnType: cty.String,
Description: "`urlencode` applies URL encoding to a given string.",
},
"values": {
Params: []function.Parameter{
{
Name: "mapping",
Type: cty.DynamicPseudoType,
},
},
ReturnType: cty.List(cty.DynamicPseudoType),
Description: "`values` takes a map and returns a list containing the values of the elements in that map.",
},
"yamldecode": {
Params: []function.Parameter{
{
Name: "src",
Type: cty.String,
},
},
ReturnType: cty.DynamicPseudoType,
Description: "`yamldecode` parses a string as a subset of YAML, and produces a representation of its value.",
},
"yamlencode": {
Params: []function.Parameter{
{
Name: "value",
Type: cty.DynamicPseudoType,
},
},
ReturnType: cty.String,
Description: "`yamlencode` encodes a given value to a string using [YAML 1.2](https://yaml.org/spec/1.2/spec.html) block syntax.",
},
"zipmap": {
Params: []function.Parameter{
{
Name: "keys",
Type: cty.List(cty.String),
},
{
Name: "values",
Type: cty.DynamicPseudoType,
},
},
ReturnType: cty.Map(cty.DynamicPseudoType),
Description: "`zipmap` constructs a map from a list of keys and a corresponding list of values.",
},
}
}
type categorized struct {
scalarFunctions map[string]schema.FunctionSignature
listFunctions map[string]schema.FunctionSignature
anyFunctions map[string]schema.FunctionSignature
}
var (
categorizedFunctions *categorized
dumpCategories = false
)
func init() {
categorizedFunctions = &categorized{
scalarFunctions: map[string]schema.FunctionSignature{},
listFunctions: map[string]schema.FunctionSignature{},
anyFunctions: map[string]schema.FunctionSignature{},
}
for k, v := range stdFunctions() {
switch {
case v.ReturnType == cty.String ||
v.ReturnType == cty.Number ||
v.ReturnType == cty.Bool:
categorizedFunctions.scalarFunctions[k] = v
case v.ReturnType.IsListType():
categorizedFunctions.listFunctions[k] = v
default:
categorizedFunctions.anyFunctions[k] = v
}
}
if dumpCategories {
printCategory := func(cat string, funcs map[string]schema.FunctionSignature) {
log.Println("functions of type", cat)
var names []string
for k := range funcs {
names = append(names, k)
}
sort.Strings(names)
for _, name := range names {
log.Println(" *", name)
}
}
printCategory("scalar", categorizedFunctions.scalarFunctions)
printCategory("list", categorizedFunctions.listFunctions)
printCategory("any", categorizedFunctions.anyFunctions)
}
}
package schema
import (
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/lang"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/schema"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/zclconf/go-cty/cty"
)
var anyBody = &schema.BodySchema{}
var anyLabelSchema = make([]*schema.LabelSchema, 0)
var anyAttribute = &schema.AttributeSchema{
Description: lang.PlainText("any value"),
IsOptional: true,
Constraint: schema.Any{},
}
var anyRequiredObjAttribute = &schema.AttributeSchema{
Description: lang.PlainText("object value"),
IsRequired: true,
Constraint: schema.Object{
AnyAttribute: schema.Any{},
},
}
var requiredMapStringAttribute = &schema.AttributeSchema{
Description: lang.PlainText("map of strings"),
IsRequired: true,
Constraint: schema.Map{Elem: schema.String{}},
}
type lookup struct {
dyn DynamicLookup
}
func (l *lookup) LabelSchema(bs schema.BlockStack) []*schema.LabelSchema {
innermost, parent := bs.Peek(0).Type, bs.Peek(1).Type
sch, ok := std[parent]
if !ok {
return anyLabelSchema
}
for t, nls := range sch.NestedBlocks {
if t == innermost {
return nls.Labels
}
}
return anyLabelSchema
}
func (l *lookup) BodySchema(bs schema.BlockStack) *schema.BodySchema {
innermostBlockType := bs.Peek(0).Type
if sch, ok := std[innermostBlockType]; ok {
return sch
}
return anyBody
}
func (l *lookup) compositeStatusSchema() *schema.AttributeSchema {
cs, ok := l.dyn.(CompositeSchemaLookup)
if !ok {
return nil
}
s := cs.CompositeSchema()
if s == nil {
return nil
}
cons, ok := s.Constraint.(schema.Object)
if !ok {
return nil
}
return cons.Attributes["status"]
}
func (l *lookup) AttributeSchema(bs schema.BlockStack, attrName string) *schema.AttributeSchema {
block := bs.Peek(0)
blockName := block.Type
switch {
case blockName == "locals":
return anyAttribute
case attrName == "body" && blockName == "composite":
if len(block.Labels) == 0 {
return anyAttribute
}
switch block.Labels[0] {
case "status":
cs := l.compositeStatusSchema()
if cs != nil {
return cs
}
return anyRequiredObjAttribute
case "connection":
return requiredMapStringAttribute
default:
return anyAttribute
}
case attrName == "body" && (blockName == "resource" || blockName == "template"):
dep, ok := dependentSchema(l.dyn, bs.Peek(0))
if ok {
return dep
}
fallthrough
default:
sch, ok := std[blockName]
if !ok {
return anyAttribute
}
as, ok := sch.Attributes[attrName]
if !ok {
return anyAttribute
}
return as
}
}
// ImpliedAttributeSchema returns a computed schema implied by the attribute expression.
// This is called when hovering over the name of an attribute.
func (l *lookup) ImpliedAttributeSchema(bs schema.BlockStack, attrName string) *schema.AttributeSchema {
block := bs.Peek(0)
if block.Type != "locals" {
return nil
}
al, ok := l.dyn.(LocalsAttributeLookup)
if !ok {
return nil
}
return al.LocalSchema(attrName)
}
func (l *lookup) Functions() map[string]schema.FunctionSignature {
return stdFunctions() // TODO: take "invoke" and user defined functions into account
}
func dependentSchema(dyn DynamicLookup, bodyBlock *hclsyntax.Block) (*schema.AttributeSchema, bool) {
attr, ok := bodyBlock.Body.Attributes["body"]
if !ok {
return nil, false
}
// ignore diags since value can be incomplete.
val, _ := attr.Expr.Value(nil)
if !val.Type().IsObjectType() && !val.Type().IsMapType() {
return nil, false
}
if val.IsNull() || !val.IsKnown() {
return nil, false
}
obj := val.AsValueMap()
apiVersion, apiOK := obj["apiVersion"]
kind, kindOK := obj["kind"]
//nolint:staticcheck
if !(apiOK && kindOK) {
return nil, false
}
//nolint:staticcheck
if !(apiVersion.Type() == cty.String && kind.Type() == cty.String) {
return nil, false
}
dynamicSchema := dyn.Schema(apiVersion.AsString(), kind.AsString())
if dynamicSchema == nil {
return nil, false
}
return dynamicSchema, true
}
package schema
import (
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/lang"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/schema"
"github.com/zclconf/go-cty/cty"
)
var std map[string]*schema.BodySchema
func basicK8sObjectSchema() schema.Object {
return schema.Object{
Attributes: map[string]*schema.AttributeSchema{
"apiVersion": {
Description: lang.PlainText("k8s api version"),
IsRequired: true,
Constraint: schema.String{},
CompletionHooks: []lang.CompletionHook{{Name: "apiVersion"}},
},
"kind": {
Description: lang.PlainText("k8s kind"),
IsRequired: true,
Constraint: schema.String{},
CompletionHooks: []lang.CompletionHook{{Name: "kind"}},
},
"metadata": {
Description: lang.PlainText("k8s metadata"),
IsOptional: true,
Constraint: schema.Object{
Attributes: schema.ObjectAttributes{
"name": {
Description: lang.Markdown("k8s object name"),
IsOptional: true,
Constraint: schema.String{},
},
"generateName": {
Description: lang.Markdown("generate k8s object name with random suffix"),
IsOptional: true,
Constraint: schema.String{},
},
"namespace": {
Description: lang.Markdown("k8s object namespace"),
IsOptional: true,
Constraint: schema.String{},
},
"labels": {
Description: lang.Markdown("k8s object labels"),
IsOptional: true,
Constraint: schema.Map{Elem: schema.String{}},
},
"annotations": {
Description: lang.Markdown("k8s object annotations"),
IsOptional: true,
Constraint: schema.Map{Elem: schema.String{}},
},
},
},
},
},
}
}
func basicBodyAttributeSchema() *schema.AttributeSchema {
return &schema.AttributeSchema{
Description: lang.PlainText("K8s object definition"),
IsRequired: true,
Constraint: basicK8sObjectSchema(),
}
}
func init() {
conditionAttributeSchema := func() *schema.AttributeSchema {
return &schema.AttributeSchema{
Description: lang.PlainText("condition"),
IsOptional: true,
Constraint: schema.Bool{},
}
}
localsBlock := func() *schema.BasicBlockSchema {
return &schema.BasicBlockSchema{
Description: lang.PlainText("local variables"),
}
}
compositeBlock := func() *schema.BasicBlockSchema {
return &schema.BasicBlockSchema{
Description: lang.PlainText("composite status or connection"),
Labels: []*schema.LabelSchema{
{
Name: "what",
Description: lang.PlainText("whether status or connection"),
AllowedValues: []string{"status", "connection"},
},
},
}
}
contextBlock := func() *schema.BasicBlockSchema {
return &schema.BasicBlockSchema{
Description: lang.PlainText("assign a value in the context"),
}
}
groupBlocks := func() map[string]*schema.BasicBlockSchema {
return map[string]*schema.BasicBlockSchema{
"locals": localsBlock(),
"group": {
Description: lang.PlainText("resource group"),
},
"resource": {
Description: lang.PlainText("resource definition"),
Labels: []*schema.LabelSchema{
{
Name: "name",
Description: lang.PlainText("crossplane resource name"),
},
},
},
"resources": {
Description: lang.PlainText("resource collection definition"),
Labels: []*schema.LabelSchema{
{
Name: "name",
Description: lang.PlainText("base name for resource collection"),
},
},
},
"composite": compositeBlock(),
"context": contextBlock(),
"requirement": {
Description: lang.PlainText("require an existing resource"),
Labels: []*schema.LabelSchema{
{
Name: "name",
Description: lang.PlainText("requirement name"),
},
},
},
}
}
topLevelBlocks := func() map[string]*schema.BasicBlockSchema {
g := groupBlocks()
g["function"] = &schema.BasicBlockSchema{
Description: lang.PlainText("function definition"),
Labels: []*schema.LabelSchema{
{
Name: "name",
Description: lang.PlainText("function name"),
},
},
}
return g
}
resChildren := func() map[string]*schema.BasicBlockSchema {
return map[string]*schema.BasicBlockSchema{
"locals": localsBlock(),
"ready": {
Description: lang.PlainText("set ready condition"),
},
"composite": compositeBlock(),
"context": contextBlock(),
}
}
std = map[string]*schema.BodySchema{
"": {
NestedBlocks: topLevelBlocks(),
},
"group": {
Description: lang.PlainText("resource group"),
Attributes: map[string]*schema.AttributeSchema{
"condition": conditionAttributeSchema(),
},
NestedBlocks: groupBlocks(),
},
"locals": {
Description: lang.PlainText("local variables"),
},
"resource": {
Description: lang.PlainText("resource declaration"),
Attributes: map[string]*schema.AttributeSchema{
"condition": conditionAttributeSchema(),
"body": basicBodyAttributeSchema(),
},
NestedBlocks: resChildren(),
},
"template": {
Description: lang.PlainText("template resource declaration"),
Attributes: map[string]*schema.AttributeSchema{
"condition": conditionAttributeSchema(),
"body": basicBodyAttributeSchema(),
},
NestedBlocks: resChildren(),
},
"resources": {
Description: lang.PlainText("resource collection declaration"),
Attributes: map[string]*schema.AttributeSchema{
"condition": conditionAttributeSchema(),
"for_each": {
IsOptional: false,
Description: lang.Markdown("the collection to iterate over"),
Constraint: schema.Any{}, // XXX: make more specific
},
"name": {
IsOptional: true,
Description: lang.Markdown("the template for the crossplane name of individual resources"),
Constraint: schema.String{},
},
},
NestedBlocks: map[string]*schema.BasicBlockSchema{
"template": {
Description: lang.PlainText("template resource definition"),
},
"locals": localsBlock(),
"composite": compositeBlock(),
"context": contextBlock(),
},
},
"composite": {
Description: lang.PlainText("composite status or connection"),
Attributes: map[string]*schema.AttributeSchema{
"body": {
Description: lang.PlainText("composite status or connection body"),
IsRequired: true,
Constraint: schema.Object{
Description: lang.PlainText("composite status or connection object"),
AllowInterpolatedKeys: false,
AnyAttribute: schema.Any{},
},
},
},
NestedBlocks: map[string]*schema.BasicBlockSchema{
"locals": localsBlock(),
},
},
"context": {
Description: lang.PlainText("context value declaration"),
Attributes: map[string]*schema.AttributeSchema{
"key": {
Description: lang.PlainText("context key"),
IsRequired: true,
Constraint: schema.String{},
},
"value": {
Description: lang.PlainText("context value"),
IsRequired: true,
Constraint: schema.Any{},
},
},
NestedBlocks: map[string]*schema.BasicBlockSchema{
"locals": localsBlock(),
},
},
"requirement": {
Description: lang.PlainText("requirement declaration"),
Attributes: map[string]*schema.AttributeSchema{
"condition": conditionAttributeSchema(),
},
NestedBlocks: map[string]*schema.BasicBlockSchema{
"locals": localsBlock(),
"select": {
Description: lang.PlainText("selection criteria"),
},
},
},
"select": {
Description: lang.PlainText("selection"),
Attributes: map[string]*schema.AttributeSchema{
"apiVersion": {
Description: lang.PlainText("k8s api version"),
IsRequired: true,
Constraint: schema.String{},
},
"kind": {
Description: lang.PlainText("k8s kind"),
IsRequired: true,
Constraint: schema.String{},
},
"matchName": {
Description: lang.PlainText("k8s object name to match"),
IsOptional: true,
Constraint: schema.String{},
},
"matchLabels": {
Description: lang.PlainText("k8s labels to match"),
IsOptional: true,
Constraint: schema.Map{
Name: "label",
Elem: schema.String{},
},
},
},
},
"function": {
Description: lang.PlainText("function definition"),
Attributes: map[string]*schema.AttributeSchema{
"body": {
Description: lang.PlainText("function body"),
IsRequired: true,
Constraint: schema.Any{},
},
"description": {
Description: lang.PlainText("function description"),
IsOptional: true,
Constraint: schema.LiteralType{Type: cty.String},
},
},
NestedBlocks: map[string]*schema.BasicBlockSchema{
"locals": localsBlock(),
"arg": {
Description: lang.PlainText("function argument"),
Labels: []*schema.LabelSchema{
{
Name: "name",
Description: lang.PlainText("argument name"),
},
},
AllowMultiple: true,
},
},
},
"arg": {
Description: lang.PlainText("argument definition"),
Attributes: map[string]*schema.AttributeSchema{
"description": {
Description: lang.PlainText("argument description"),
IsOptional: true,
Constraint: schema.LiteralType{Type: cty.String},
},
"default": {
Description: lang.PlainText("default value"),
IsOptional: true,
Constraint: schema.LiteralType{Type: cty.DynamicPseudoType},
},
},
},
"ready": {
Description: lang.PlainText("ready condition"),
Attributes: map[string]*schema.AttributeSchema{
"value": {
Description: lang.PlainText("ready status value (READY_UNSPECIFIED, READY_TRUE, or READY_FALSE)"),
IsRequired: true,
Constraint: schema.String{},
},
},
},
}
}
// Package target provides definition and reference information for a function-hcl module.
package target
import (
ourschema "github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/funchcl/schema"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/schema"
"github.com/hashicorp/hcl/v2"
)
// Node represents a specific leaf or intermediate target in a target tree.
type Node struct {
Name string // the name using which the node is referenced
IsContainer bool // container node that has an implied schema of "any object"
Schema *schema.AttributeSchema // the schema for non-container nodes
NameRange hcl.Range // the range where the name of the node is declared
Definition hcl.Range // the range where the node is defined
Children []*Node // the child nodes of this one.
}
func attrsForNodes(nodes []*Node) map[string]*schema.AttributeSchema {
attrs := map[string]*schema.AttributeSchema{}
for _, child := range nodes {
attrs[child.Name] = child.AsSchema()
}
return attrs
}
func (n *Node) AsSchema() *schema.AttributeSchema {
if !n.IsContainer {
return n.Schema
}
return &schema.AttributeSchema{
Constraint: schema.Object{
Name: n.Name,
Attributes: attrsForNodes(n.Children),
},
}
}
// Tree is a tree of nodes accessible in a given scope.
type Tree struct {
root *Node // a synthetic container for the actual, multiple, roots of the tree
}
func newTree(roots ...*Node) *Tree {
return &Tree{
root: &Node{
Children: roots,
},
}
}
// Roots returns the roots of the tree. These are the "top-level variables"
// like locals, `req`, `self`, `each` etc.
func (t *Tree) Roots() []*Node {
return t.root.Children
}
// AsSchema returns the contents of the tree as an attribute schema of a top-level
// object that represents the tree as a whole.
func (t *Tree) AsSchema() *schema.AttributeSchema {
return &schema.AttributeSchema{
Constraint: schema.Object{
Attributes: attrsForNodes(t.Roots()),
},
}
}
// Targets provides a mechanism to find all accessible symbols at a given position.
// It contains a global tree for references that are accessible globally (e.g.
// file-scoped variables, req.composite etc.) and stores extra information for
// block scoped locals, self aliases for resources and collections. Given a position in a
// file, these extras allow construction of a "visible" tree that contains all
// accessible references from that position.
type Targets struct {
CompositeSchema *schema.AttributeSchema // the schema used for the composite or nil
globals *Tree // the global tree visible from any position
scopedLocalsByFile map[string][]*scopedLocal // scoped locals accessible in a file range
resourceByFile map[string][]*alias // the meaning of self.resource within a range
collectionByFile map[string][]*alias // the meaning of self.resources within a range
declarationRanges []hcl.Range // ranges where resources are declared
}
// VisibleTreeAt returns a tree that contains all references visible from the supplied
// position in a specific file.
func (t *Targets) VisibleTreeAt(parent *hcl.Block, file string, pos hcl.Pos) *Tree {
return t.visibleTreeAt(parent, file, pos)
}
type (
DefToRefs map[hcl.Range][]hcl.Range // map of definition ranges to multiple reference ranges
RefsToDef map[hcl.Range]hcl.Range // maps of reference ranges to definition ranges
)
// ReferenceMap provides mappings between definitions and references in both directions.
type ReferenceMap struct {
DefToRefs DefToRefs
RefsToDef RefsToDef
}
// FindDefinitionFromReference returns the definition range for a reference range that includes
// the supplied position.
func (p *ReferenceMap) FindDefinitionFromReference(filename string, pos hcl.Pos) *hcl.Range {
for ref, def := range p.RefsToDef {
if ref.Filename == filename && ref.ContainsPos(pos) {
if def.Filename == "" { // pseudo-range that doesn't have a definition
return nil
}
return &def
}
}
// account for when people try to find a definition when the cursor is on the definition itself
for def := range p.DefToRefs {
if def.Filename == filename && def.ContainsPos(pos) {
return &def
}
}
return nil
}
// FindReferencesFromDefinition returns reference ranges for a definition range that includes
// the supplied position.
func (p *ReferenceMap) FindReferencesFromDefinition(filename string, pos hcl.Pos) []hcl.Range {
for ref, def := range p.DefToRefs {
if ref.Filename == filename && ref.ContainsPos(pos) {
return def
}
}
return nil
}
// BuildTargets builds targets for a module.
func BuildTargets(files map[string]*hcl.File, dyn ourschema.DynamicLookup, compositeSchema *schema.AttributeSchema) *Targets {
return buildTargets(files, dyn, compositeSchema)
}
// BuildReferenceMap builds the reference map for a module.
func BuildReferenceMap(files map[string]*hcl.File, targets *Targets) *ReferenceMap {
return buildReferenceMap(files, targets)
}
// SchemaForRelativeTraversal returns the child schema implied by the root schema and the supplied traversal.
// The traversal does not have to be a relative traversal; an absolute one is also ok.
// It returns an unknown schema if a schema could not be found.
func SchemaForRelativeTraversal(root *schema.AttributeSchema, traversal hcl.Traversal) (ret *schema.AttributeSchema) {
s := processRelativeTraversal(root, traversal)
if s == nil {
return unknownSchema
}
return s
}
// SubSchema returns a known schema at the supplied path relative to the supplied root schema.
// It returns an unknown schema if one could not be found.
func SubSchema(s *schema.AttributeSchema, path ...string) *schema.AttributeSchema {
s = subSchema(s, path...)
if s == nil {
return unknownSchema
}
return s
}
package target
import (
ourschema "github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/funchcl/schema"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/funchcl/typeutils"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/schema"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/zclconf/go-cty/cty"
)
func (l *localsCollection) schemaFromFunctionCall(e *hclsyntax.FunctionCallExpr) *schema.AttributeSchema {
sig, ok := ourschema.StandardFunctions[e.Name]
if !ok {
return nil
}
switch sig.ReturnType {
case cty.String, cty.Number, cty.Bool:
return &schema.AttributeSchema{Constraint: typeutils.TypeConstraint(sig.ReturnType)}
}
switch e.Name {
// for these functions the schema of the result is a schema found for any of the args.
case "coalesce",
"coalescelist",
"concat",
"distinct",
"reverse",
"slice",
"sort",
"transpose",
"try",
"setintersection",
"setsubtract",
"setunion":
return l.findOneFrom(e.Args...)
case "toset":
if len(e.Args) > 0 {
s := l.impliedSchema(e.Args[0])
if s == nil {
return nil
}
if listCons, ok := s.Constraint.(schema.List); ok {
return &schema.AttributeSchema{Constraint: schema.Set{Elem: listCons.Elem}}
}
}
// the schema is the union set of the schema from all args
case "merge":
return l.findUnionObjectType(e.Args...)
// unwrap a list schema at the first arg position, if found
case "element", "flatten", "one":
if len(e.Args) > 0 {
s := l.impliedSchema(e.Args[0])
if s == nil {
return nil
}
switch cons := s.Constraint.(type) {
case schema.List:
return &schema.AttributeSchema{Constraint: cons.Elem}
}
}
return nil
// same sig as the first argument
case "matchkeys":
if len(e.Args) > 0 {
return l.impliedSchema(e.Args[0])
}
// list of some list
case "chunklist":
s := l.findOneFrom(e.Args...)
if s == nil {
return nil
}
return &schema.AttributeSchema{Constraint: schema.List{Elem: s.Constraint}}
// infer from default, or unwrap first argument map
case "lookup":
if len(e.Args) > 1 {
s := l.impliedSchema(e.Args[1])
if s != nil {
return s
}
}
if len(e.Args) > 0 {
s := l.impliedSchema(e.Args[0])
switch cons := s.Constraint.(type) {
case schema.Map:
return &schema.AttributeSchema{Constraint: cons.Elem}
}
}
return nil
// unwrap map values
case "values":
if len(e.Args) > 0 {
s := l.impliedSchema(e.Args[0])
switch cons := s.Constraint.(type) {
case schema.Map:
return &schema.AttributeSchema{Constraint: schema.List{Elem: cons.Elem}}
}
}
// too complex, weird rules, not worth it...
case "setproduct",
"tolist",
"tomap",
"zipmap":
return nil
}
if sig.ReturnType.IsListType() {
return &schema.AttributeSchema{
Constraint: schema.List{
Elem: typeutils.TypeConstraint(sig.ReturnType.ElementType()),
},
}
}
return nil
}
package target
import (
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/funchcl/typeutils"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/schema"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/zclconf/go-cty/cty"
)
// this file contains the implementation of inferring schemas for local variables.
// For simple cases like constant string and some others, this can be done easily.
//
// In addition, we also implement "reference chasing".
// That is, if a local is defined as req.composite.spec, then assign it the attribute schema for
// spec. This works recursively such that derived locals based on another local that has a schema
// gets the appropriate sub-schema.
// local is a local variable found at a specific scope with an associated expression.
type local struct {
name string
expr hclsyntax.Expression
enhancedSchema *schema.AttributeSchema
}
// localsCollection is a collection of locals at a specific scope
type localsCollection struct {
parent *localsCollection // parent for this collection, if any
parentCollection string // if it is a descendant of a resources block
parentResource string // if it is a descendant of a resource block
locals map[string]*local // local variables
scopedTo hcl.Range // scope for collection, zero-value is root
children []*localsCollection // child scopes
// attributes set before enhancement
globalSchema *schema.AttributeSchema
fileSource func(e hclsyntax.Expression) []byte
}
func newLocalsCollection(parent *localsCollection, scopedTo hcl.Range) *localsCollection {
lc := &localsCollection{
parent: parent,
locals: map[string]*local{},
scopedTo: scopedTo,
}
if parent != nil {
parent.children = append(parent.children, lc)
lc.parentCollection = parent.parentCollection
}
return lc
}
func (l *localsCollection) computeSchemas(globalSchema *schema.AttributeSchema, fileSource func(e hclsyntax.Expression) []byte) {
l.globalSchema = globalSchema
l.fileSource = fileSource
for _, v := range l.locals {
l.computeSchema(v)
}
}
func (l *localsCollection) computeSchema(loc *local) {
if loc.enhancedSchema != nil {
return // already computed or circular ref
}
// first mark as seen to avoid infinite loops due to circular refs
loc.enhancedSchema = unknownSchema
// then try and compute the real thing
e := l.impliedSchema(loc.expr)
// for the `each` variable create the kv schema based on the collection
// schema presumably returned.
if loc.name == "each" {
e = typeutils.KVSchema(e)
}
if e != nil {
loc.enhancedSchema = e
}
}
func (l *localsCollection) findOneFrom(expressions ...hclsyntax.Expression) *schema.AttributeSchema {
for _, expr := range expressions {
s := l.impliedSchema(expr)
if s != nil {
return s
}
}
return nil
}
func (l *localsCollection) findUnionObjectType(expressions ...hclsyntax.Expression) *schema.AttributeSchema {
attrs := map[string]*schema.AttributeSchema{}
for _, expr := range expressions {
s := l.impliedSchema(expr)
if s == nil {
return nil
}
// if it's a map, assume other args are also maps
if _, ok := s.Constraint.(schema.Map); ok {
return s
}
cons, ok := s.Constraint.(schema.Object)
if !ok {
return nil
}
for k, v := range cons.Attributes {
seen, ok := attrs[k]
if ok {
if seen != unknownSchema {
v = seen
}
}
attrs[k] = v
}
}
return &schema.AttributeSchema{Constraint: schema.Object{Attributes: attrs}}
}
func (l *localsCollection) impliedSchema(expr hclsyntax.Expression) (ret *schema.AttributeSchema) {
switch e := expr.(type) {
// literal values
case *hclsyntax.LiteralValueExpr:
v, diags := expr.Value(nil)
if diags.HasErrors() || !v.IsWhollyKnown() {
return nil
}
return &schema.AttributeSchema{Constraint: typeutils.TypeConstraint(v.Type())}
case *hclsyntax.TemplateExpr:
return &schema.AttributeSchema{Constraint: schema.String{}}
// from function call signatures
case *hclsyntax.FunctionCallExpr:
return l.schemaFromFunctionCall(e)
// from traversals
case *hclsyntax.ScopeTraversalExpr:
return l.schemaFromScopeTraversal(e)
case *hclsyntax.RelativeTraversalExpr:
return l.schemaFromRelativeTraversal(e)
case *hclsyntax.IndexExpr:
return l.schemaFromIndexExpression(e)
case *hclsyntax.SplatExpr:
return l.schemaFromSplat(e)
// from operators: implied by operator used
case *hclsyntax.BinaryOpExpr:
return l.operationToSchema(e.Op)
case *hclsyntax.UnaryOpExpr:
return l.operationToSchema(e.Op)
// descend into these
case *hclsyntax.ObjectConsExpr:
return l.objectSchemaFromItems(e.Items)
case *hclsyntax.ConditionalExpr:
return l.findOneFrom(e.TrueResult, e.FalseResult)
case *hclsyntax.TupleConsExpr:
inner := l.findOneFrom(e.Exprs...)
if inner == nil {
return nil
}
return &schema.AttributeSchema{Constraint: schema.List{Elem: inner.Constraint}}
case *hclsyntax.ParenthesesExpr:
return l.impliedSchema(e.Expression)
case *hclsyntax.TemplateWrapExpr:
return l.impliedSchema(e.Wrapped)
case *hclsyntax.ExprSyntaxError:
return
// we don't know how to process these yet
case *hclsyntax.ForExpr:
return
}
return
}
func (l *localsCollection) objectSchemaFromItems(items []hclsyntax.ObjectConsItem) *schema.AttributeSchema {
attrs := map[string]*schema.AttributeSchema{}
for _, item := range items {
k, diags := item.KeyExpr.Value(nil)
if diags.HasErrors() {
return nil
}
if k.Type() != cty.String {
return nil
}
vs := l.impliedSchema(item.ValueExpr)
if vs == nil {
vs = unknownSchema
}
attrs[k.AsString()] = vs
}
return &schema.AttributeSchema{Constraint: schema.Object{Attributes: attrs}}
}
func (l *localsCollection) operationToSchema(op *hclsyntax.Operation) *schema.AttributeSchema {
switch op {
case hclsyntax.OpAdd,
hclsyntax.OpSubtract,
hclsyntax.OpMultiply,
hclsyntax.OpDivide,
hclsyntax.OpModulo,
hclsyntax.OpNegate:
return &schema.AttributeSchema{Constraint: schema.Number{}}
case hclsyntax.OpEqual,
hclsyntax.OpNotEqual,
hclsyntax.OpLessThan,
hclsyntax.OpLessThanOrEqual,
hclsyntax.OpGreaterThan,
hclsyntax.OpGreaterThanOrEqual,
hclsyntax.OpLogicalAnd,
hclsyntax.OpLogicalOr,
hclsyntax.OpLogicalNot:
return &schema.AttributeSchema{Constraint: schema.Bool{}}
default:
return nil
}
}
package target
import (
"strings"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
)
func hasLocals(b *hclsyntax.Block) bool {
if b.Body == nil {
return false
}
for _, block := range b.Body.Blocks {
if block.Type == "locals" {
return true
}
}
return false
}
func findDefinition(tree *Tree, pathElements []string) *hcl.Range {
rootName := pathElements[0]
var current *Node
rest := pathElements[1:]
for _, root := range tree.Roots() {
if root.Name == rootName {
current = root
break
}
}
if current == nil {
return nil
}
for _, pathElement := range rest {
found := false
for _, node := range current.Children {
if node.Name == pathElement {
found = true
current = node
break
}
}
if !found {
break
}
}
return ¤t.Definition
}
func walkRefs(body *hclsyntax.Body, fileContent []byte, targets *Targets, visibleTree *Tree, p *ReferenceMap) {
if body == nil {
return
}
for _, attr := range body.Attributes {
_ = hclsyntax.VisitAll(attr.Expr, func(node hclsyntax.Node) hcl.Diagnostics {
expr, ok := node.(*hclsyntax.ScopeTraversalExpr)
if !ok {
return nil
}
if expr.Traversal.IsRelative() { // don't know how to process relative traversals
return nil
}
var pathElements []string
for _, traverser := range expr.Traversal {
nameBytes := traverser.SourceRange().SliceBytes(fileContent)
pathElements = append(pathElements, extractTraversalIdentifier(nameBytes))
}
defRange := findDefinition(visibleTree, pathElements)
if defRange != nil {
r := *defRange
p.RefsToDef[(expr.Range())] = r
p.DefToRefs[r] = append(p.DefToRefs[r], expr.Range())
}
return nil
})
}
getVisibleTree := func(b *hclsyntax.Block) *Tree {
return targets.VisibleTreeAt(b.AsHCLBlock(), b.TypeRange.Filename, b.TypeRange.End)
}
for _, block := range body.Blocks {
if block.Body == nil {
continue
}
childTree := visibleTree
switch {
// when new scopes are introduced, recalculate the visible tree
case block.Type == "resource" || block.Type == "resources" || block.Type == "template" || block.Type == "function":
childTree = getVisibleTree(block)
// locals introduced new scoped variables
case hasLocals(block):
childTree = getVisibleTree(block)
}
walkRefs(block.Body, fileContent, targets, childTree, p)
}
}
func extractTraversalIdentifier(bytes []byte) string {
s := string(bytes)
if strings.HasPrefix(s, ".") {
s = s[1:]
} else if strings.HasPrefix(s, "[") && strings.HasSuffix(s, "]") {
s = s[1 : len(s)-1]
}
if strings.HasPrefix(s, `"`) && strings.HasSuffix(s, `"`) {
s = s[1 : len(s)-1]
}
return s
}
func buildReferenceMap(files map[string]*hcl.File, targets *Targets) *ReferenceMap {
p := &ReferenceMap{
DefToRefs: DefToRefs{},
RefsToDef: RefsToDef{},
}
for _, r := range targets.declarationRanges {
p.DefToRefs[r] = nil // ensure key for all definitions
}
for _, f := range files {
// start with the global visible tree
visibleTree := targets.globals
body := f.Body.(*hclsyntax.Body)
walkRefs(body, f.Bytes, targets, visibleTree, p)
}
return p
}
package target
import (
"fmt"
"log"
ourschema "github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/funchcl/schema"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/lang"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/schema"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
)
func (t *Tree) add(node *Node, pathElements ...string) {
current := t.root
found := false
node.IsContainer = false // force non-container leaf
for _, el := range pathElements {
found = false
for _, child := range current.Children {
if child.Name == el {
if !child.IsContainer {
log.Printf("error: attempt to replace child with name %q with a container", el)
return
}
found = true
current = child
break
}
}
if !found {
tmp := &Node{
Name: el,
IsContainer: true,
}
current.Children = append(current.Children, tmp)
current = tmp
}
}
current.Children = append(current.Children, node)
}
type scopedLocal struct {
name string // the local variable name
definition hcl.Range // the range where the node is defined
schema *schema.AttributeSchema // the read schema for the local
accessibleFrom hcl.Range // the range from which this local is accessible
}
type alias struct {
definition hcl.Range // the range where the node is defined
accessibleFrom hcl.Range // the range from which the node can be accessed
schema *schema.AttributeSchema // the read schema for the local
}
func buildTargets(files map[string]*hcl.File, dyn ourschema.DynamicLookup, compositeSchema *schema.AttributeSchema) *Targets {
compSchema := compositeSchema
if compSchema == nil {
compSchema = unknownSchema
}
t := newTree()
t.add(&Node{
Name: "composite",
Schema: ourschema.WithoutAPIVersionAndKind(compSchema),
}, "req")
t.add(&Node{
Name: "composite_connection",
Schema: &schema.AttributeSchema{Constraint: schema.Map{Elem: schema.String{}}},
}, "req")
t.add(&Node{
Name: "context",
Schema: unknownSchema,
}, "req")
ret := &Targets{
CompositeSchema: compositeSchema,
globals: t,
scopedLocalsByFile: map[string][]*scopedLocal{},
resourceByFile: map[string][]*alias{},
collectionByFile: map[string][]*alias{},
}
rootLocals := newLocalsCollection(nil, hcl.Range{})
for filename, f := range files {
body := f.Body.(*hclsyntax.Body)
bs := schema.NewBlockStack()
walk(body, bs, rootLocals, func(bs schema.BlockStack, coll *localsCollection) *localsCollection {
out := coll // by default this does not change when scope does not change
current := bs.Peek(0)
parent := bs.Peek(1)
switch current.Type {
case "resources":
// set up the targetable each alias for the collection
eachAttr := current.Body.Attributes["for_each"]
var eachExpr hclsyntax.Expression
if eachAttr != nil {
eachExpr = eachAttr.Expr
}
// always create a new locals scope. When processing locals
// take this into account because the scope of the `each` variable
// is the same as the scope of locals just under resources.
out = newLocalsCollection(coll, current.Range())
if eachExpr != nil {
// add a scoped collection
ret.scopedLocalsByFile[filename] = append(ret.scopedLocalsByFile[filename], &scopedLocal{
name: "each",
schema: unknownSchema,
definition: eachAttr.NameRange,
accessibleFrom: current.Range(),
})
out.locals["each"] = &local{
name: "each",
expr: eachExpr,
}
}
// a resource implies a global and local addressable entity as long as it has a name.
case "resource":
if len(current.Labels) == 0 {
log.Printf("resource block at %s, %v does not have a name label", filename, current.TypeRange)
return out
}
sch := ourschema.WithStatusOnly(ourschema.DependentSchemaOrDefault(dyn, current))
defRange := hcl.RangeBetween(current.TypeRange, current.OpenBraceRange)
ret.declarationRanges = append(ret.declarationRanges, defRange)
// add a req.resource.<foo> entry and a req.connection.<foo> entry
t.add(&Node{
Name: current.Labels[0],
Schema: sch,
Definition: defRange,
NameRange: current.LabelRanges[0],
}, "req", "resource")
t.add(&Node{
Name: current.Labels[0],
Schema: &schema.AttributeSchema{Constraint: schema.Map{Elem: schema.String{}}},
Definition: defRange,
NameRange: current.LabelRanges[0],
}, "req", "connection")
// set up the targetable self alias for the resource
ret.resourceByFile[filename] = append(ret.resourceByFile[filename], &alias{
schema: sch,
definition: defRange,
accessibleFrom: current.Range(),
})
// a template implies a collection and a resource, provided the correct structure is defined
case "template":
if parent.Type != "resources" {
log.Printf("template block at %s, %v does not have a resources parent, found %q", filename, current.TypeRange, parent.Type)
return out
}
if len(parent.Labels) == 0 {
log.Printf("resources block at %s, %v does not have a name label", filename, parent.TypeRange)
return out
}
sch := ourschema.DependentSchemaOrDefault(dyn, current)
defRange := hcl.RangeBetween(parent.TypeRange, parent.OpenBraceRange)
// add a req.resources.<foo> entry and a req.connections.<foo> entry
t.add(&Node{
Name: parent.Labels[0],
Schema: &schema.AttributeSchema{
Constraint: schema.List{Elem: sch.Constraint},
},
Definition: defRange,
NameRange: parent.LabelRanges[0],
}, "req", "resources")
t.add(&Node{
Name: parent.Labels[0],
Schema: &schema.AttributeSchema{
Constraint: schema.List{Elem: schema.Map{Elem: schema.String{}}},
},
Definition: defRange,
NameRange: parent.LabelRanges[0],
}, "req", "connections")
// set up the targetable self alias for the collection
ret.collectionByFile[filename] = append(ret.collectionByFile[filename], &alias{
schema: sch,
definition: hcl.RangeBetween(parent.TypeRange, parent.OpenBraceRange),
accessibleFrom: parent.Range(),
})
// set up the targetable self alias for the resource
ret.resourceByFile[filename] = append(ret.resourceByFile[filename], &alias{
schema: sch,
definition: defRange,
accessibleFrom: hcl.RangeBetween(current.TypeRange, current.OpenBraceRange),
})
// locals are accessible in parent scope or global for file scoped locals
case "locals":
if parent.Type != "" && parent.Type != "resources" {
out = newLocalsCollection(coll, parent.Range()) // create child scope or update root in place
n := 1
for {
b := bs.Peek(n)
n++
if b.Type == "" {
break
}
if b.Type == "function" { // a function block cannot see the outer world
break
}
if b.Type == "resource" && len(b.Labels) > 0 {
out.parentResource = b.Labels[0]
}
if b.Type == "resources" && len(b.Labels) > 0 {
out.parentCollection = b.Labels[0]
}
}
}
for attrName, attr := range current.Body.Attributes {
// if root level, add directly to the global tree
if parent.Type == "" {
node := &Node{
Name: attrName,
Schema: unknownSchema,
Definition: attr.NameRange,
NameRange: attr.NameRange,
}
t.add(node)
} else {
// add a scoped collection
ret.scopedLocalsByFile[filename] = append(ret.scopedLocalsByFile[filename], &scopedLocal{
name: attrName,
schema: unknownSchema,
definition: attr.NameRange,
accessibleFrom: parent.Range(),
})
}
out.locals[attrName] = &local{
name: attrName,
expr: attr.Expr,
}
}
// an arg is a local in function scope
case "arg":
if len(current.Labels) == 0 {
log.Printf("arg block at %s, %v does not have a name label", filename, current.TypeRange)
return out
}
ret.scopedLocalsByFile[filename] = append(ret.scopedLocalsByFile[filename], &scopedLocal{
name: current.Labels[0],
schema: &schema.AttributeSchema{Constraint: schema.Any{}},
definition: hcl.RangeBetween(current.TypeRange, current.OpenBraceRange),
accessibleFrom: parent.Range(),
})
}
return out
})
}
ret.enhanceLocalSchemas(rootLocals, t.AsSchema(), func(e hclsyntax.Expression) []byte {
f, ok := files[e.Range().Filename]
if !ok {
panic(fmt.Sprintf("file %s not found in files to get source", e.Range().Filename))
}
return f.Bytes
})
return ret
}
func (t *Targets) visibleTreeAt(parent *hcl.Block, file string, pos hcl.Pos) *Tree {
addLocalsToTree := func(tree *Tree) {
for _, loc := range t.scopedLocalsByFile[file] {
if loc.accessibleFrom.ContainsPos(pos) {
tree.add(&Node{
Name: loc.name,
Schema: loc.schema,
Definition: loc.definition,
})
}
}
}
switch {
case parent == nil: // at top-level, nothing is accessible
return newTree()
// special case for functions where global references are not accessible
case parent.Type == "function":
funcTree := newTree()
addLocalsToTree(funcTree)
return funcTree
default:
ret := &Tree{
root: &Node{
Children: t.globals.root.Children,
},
}
addLocalsToTree(ret)
for _, res := range t.resourceByFile[file] {
if res.accessibleFrom.ContainsPos(pos) {
ret.add(&Node{
Name: "name",
Schema: &schema.AttributeSchema{
Description: lang.PlainText("crossplane resource name"),
Constraint: schema.String{},
},
Definition: res.definition,
}, "self")
ret.add(&Node{
Name: "connection",
Schema: &schema.AttributeSchema{
Description: lang.PlainText("resource connection details"),
Constraint: schema.Map{
Elem: schema.String{},
},
},
Definition: res.definition,
}, "self")
ret.add(&Node{
Name: "resource",
Schema: res.schema,
Definition: res.definition,
}, "self")
}
}
for _, res := range t.collectionByFile[file] {
if res.accessibleFrom.ContainsPos(pos) {
ret.add(&Node{
Name: "basename",
Schema: &schema.AttributeSchema{
Description: lang.PlainText("base name of resource collection"),
Constraint: schema.String{},
},
Definition: res.definition,
}, "self")
ret.add(&Node{
Name: "connections",
Schema: &schema.AttributeSchema{
Description: lang.PlainText("resource collection connection details"),
Constraint: schema.List{
Elem: schema.Map{
Elem: schema.String{},
},
},
},
Definition: res.definition,
}, "self")
ret.add(&Node{
Name: "resources",
Schema: &schema.AttributeSchema{
Description: lang.PlainText("resource collection details"),
Constraint: schema.List{
Elem: res.schema.Constraint,
},
},
Definition: res.definition,
}, "self")
}
}
return ret
}
}
func walk(body *hclsyntax.Body, bs schema.BlockStack, coll *localsCollection, callback func(bs schema.BlockStack, coll *localsCollection) *localsCollection) {
for _, block := range body.Blocks {
bs.Push(block)
child := callback(bs, coll)
walk(block.Body, bs, child, callback)
bs.Pop()
}
}
func (t *Targets) enhanceLocalSchemas(coll *localsCollection, globalSchema *schema.AttributeSchema, fileSource func(e hclsyntax.Expression) []byte) {
coll.computeSchemas(globalSchema, fileSource)
for k, v := range coll.locals {
if v.enhancedSchema == unknownSchema {
continue
}
// update the correct object to account for the schema
if coll.parent == nil { // at root, look directly into the globals tree
found := false
for _, root := range t.globals.Roots() {
if root.Name == k {
found = true
root.Schema = v.enhancedSchema
break
}
}
if !found {
log.Printf("internal error: local %s not found in global tree", k)
}
continue
}
// not at root - find the local in the scoped structure.
found := false
for _, sl := range t.scopedLocalsByFile[coll.scopedTo.Filename] {
if sl.accessibleFrom == coll.scopedTo {
if sl.name == k {
found = true
sl.schema = v.enhancedSchema
break
}
}
}
if !found {
log.Printf("internal error: scoped local %s (%v) not found in scoped locals collection", k, coll.scopedTo)
}
}
for _, child := range coll.children {
t.enhanceLocalSchemas(child, globalSchema, fileSource)
}
}
package target
import (
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/lang"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/schema"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/zclconf/go-cty/cty"
)
func (l *localsCollection) variable(name string) *local {
current := l
for current != nil {
if loc, ok := current.locals[name]; ok {
if current == l {
l.computeSchema(loc)
}
if loc.enhancedSchema == unknownSchema {
return nil
}
return loc
}
current = current.parent
}
return nil
}
func (l *localsCollection) selfSchema() *schema.AttributeSchema {
if l.parentResource == "" && l.parentCollection == "" {
return nil
}
attrs := map[string]*schema.AttributeSchema{}
if l.parentCollection != "" {
attrs["basename"] = &schema.AttributeSchema{Constraint: schema.String{}}
attrs["connections"] = &schema.AttributeSchema{Constraint: schema.List{Elem: schema.Map{Elem: schema.String{}}}}
attrs["resources"] = SubSchema(l.globalSchema, "req", "resources", l.parentCollection)
}
if l.parentResource != "" {
attrs["name"] = &schema.AttributeSchema{Constraint: schema.String{}}
attrs["connection"] = &schema.AttributeSchema{Constraint: &schema.Map{Elem: schema.String{}}}
attrs["resource"] = SubSchema(l.globalSchema, "req", "resource", l.parentResource)
}
return &schema.AttributeSchema{
Constraint: schema.Object{
Name: "self",
Description: lang.PlainText("self reference"),
Attributes: attrs,
},
}
}
func (l *localsCollection) asStringValue(key hclsyntax.Expression) (string, bool) {
switch key := key.(type) {
// allow a single variable as an indirection mechanism - this should account for most cases
case *hclsyntax.ScopeTraversalExpr:
if len(key.Traversal) != 1 {
return "", false
}
rootVar := key.Traversal[0].(hcl.TraverseRoot)
v := l.variable(rootVar.Name)
if v == nil {
return "", false
}
_, ok1 := v.expr.(*hclsyntax.TemplateExpr)
_, ok2 := v.expr.(*hclsyntax.LiteralValueExpr)
if !ok1 && !ok2 { // prevent cycles with this check otherwise foo = foo will keep on spinning
return "", false
}
return l.asStringValue(v.expr)
default:
v, diags := key.Value(nil)
if diags.HasErrors() || !v.IsWhollyKnown() {
return "", false
}
if v.Type() != cty.String {
return "", false
}
return v.AsString(), true
}
}
func (l *localsCollection) schemaFromScopeTraversal(e *hclsyntax.ScopeTraversalExpr) *schema.AttributeSchema {
t := e.Traversal
if len(t) == 0 {
return nil
}
root := t[0].(hcl.TraverseRoot).Name
switch root {
case "req": // can only occur in the global tree
return processRelativeTraversal(l.globalSchema, t)
case "self":
return processRelativeTraversal(l.selfSchema(), t[1:])
default: // must be a local at our scope or a parent scope, note that each is treated as a local
v := l.variable(root)
if v == nil {
return nil
}
return processRelativeTraversal(v.enhancedSchema, t[1:])
}
}
func (l *localsCollection) schemaFromIndexExpression(e *hclsyntax.IndexExpr) *schema.AttributeSchema {
collSchema := l.impliedSchema(e.Collection)
if collSchema == nil {
return nil
}
cons := collSchema.Constraint
// for maps and list we do not care about what the index is
// the return value is the element schema for the inner element.
switch cons := cons.(type) {
case schema.Map:
return &schema.AttributeSchema{Constraint: cons.Elem}
case schema.List:
return &schema.AttributeSchema{Constraint: cons.Elem}
// for objects, we need to eval the key and turn it into a string, if possible
case schema.Object:
key, ok := l.asStringValue(e.Key)
if !ok {
return nil
}
attr, ok := cons.Attributes[key]
if !ok {
return nil
}
return &schema.AttributeSchema{Constraint: attr.Constraint}
}
return nil
}
func (l *localsCollection) schemaFromRelativeTraversal(e *hclsyntax.RelativeTraversalExpr) *schema.AttributeSchema {
sourceSchema := l.impliedSchema(e.Source)
return processRelativeTraversal(sourceSchema, e.Traversal)
}
func (l *localsCollection) schemaFromSplat(e *hclsyntax.SplatExpr) *schema.AttributeSchema {
sourceSchema := l.impliedSchema(e.Source)
if sourceSchema == nil {
return nil
}
listCons, ok := sourceSchema.Constraint.(schema.List)
if !ok {
return nil
}
_, anonEach := e.Each.(*hclsyntax.AnonSymbolExpr)
if e.Each == nil || anonEach {
return sourceSchema
}
rel, ok := e.Each.(*hclsyntax.RelativeTraversalExpr)
if !ok {
return nil
}
// unwrap
rootSchema := &schema.AttributeSchema{Constraint: listCons.Elem}
elementSchema := processRelativeTraversal(rootSchema, rel.Traversal)
if elementSchema == nil {
return nil
}
// wrap
return &schema.AttributeSchema{Constraint: schema.List{Elem: elementSchema.Constraint}}
}
package target
import (
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/schema"
"github.com/hashicorp/hcl/v2"
"github.com/zclconf/go-cty/cty"
)
var unknownSchema = &schema.AttributeSchema{Constraint: schema.Any{}}
// subSchema returns the sub-schema for the supplied traversal if possible, or nil otherwise.
func subSchema(s *schema.AttributeSchema, path ...string) *schema.AttributeSchema {
if s == nil {
return nil
}
if len(path) == 0 {
return s
}
objCons, ok := s.Constraint.(schema.Object)
if !ok {
return nil
}
sub, ok := objCons.Attributes[path[0]]
if !ok {
return nil
}
return subSchema(sub, path[1:]...)
}
// processRelativeTraversal returns the child schema implied by the root schema and the supplied traversal.
// The traversal does not have to be a relative traversal; an absolute one is also ok.
// It returns nil if a schema could not be found.
func processRelativeTraversal(root *schema.AttributeSchema, traversal hcl.Traversal) (ret *schema.AttributeSchema) {
defer func() {
if ret == nil {
ret = unknownSchema
}
}()
outer:
for _, child := range traversal {
if root == nil {
break
}
// if current is a map or a list, then indexing it further just yields the element type
switch cons := root.Constraint.(type) {
case schema.Map:
root = &schema.AttributeSchema{Constraint: cons.Elem}
continue outer
case schema.List:
root = &schema.AttributeSchema{Constraint: cons.Elem}
continue outer
}
if _, ok := root.Constraint.(schema.Object); !ok {
return nil
}
switch child := child.(type) {
case hcl.TraverseRoot: // root is allowed
root = subSchema(root, child.Name)
case hcl.TraverseAttr:
root = subSchema(root, child.Name)
case hcl.TraverseIndex:
if child.Key.Type() != cty.String {
return nil
}
root = subSchema(root, child.Key.AsString())
}
}
return root
}
package typeutils
import (
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/schema"
"github.com/zclconf/go-cty/cty"
)
// KVSchema returns a schema for the `each` variable. It is an object with a key and value property.
// If this cannot be derived, the function returns nil.
func KVSchema(base *schema.AttributeSchema) *schema.AttributeSchema {
if base == nil {
return nil
}
var ret *schema.AttributeSchema
switch cons := base.Constraint.(type) {
case schema.List:
ct, ok := cons.ConstraintType()
if ok {
ret = collectionSchemaFromType(ct)
}
case schema.Map:
ct, ok := cons.ConstraintType()
if ok {
ret = collectionSchemaFromType(ct)
}
case schema.Set:
ct, ok := cons.ConstraintType()
if ok {
ret = collectionSchemaFromType(ct)
}
}
return ret
}
// TypeConstraint returns a constraint for the supplied type.
func TypeConstraint(t cty.Type) schema.Constraint {
switch {
case t == cty.String:
return schema.String{}
case t == cty.Number:
return schema.Number{}
case t == cty.Bool:
return schema.Bool{}
case t.IsObjectType():
types := t.AttributeTypes()
inner := map[string]*schema.AttributeSchema{}
for name, typ := range types {
inner[name] = &schema.AttributeSchema{Constraint: TypeConstraint(typ)}
}
return schema.Object{Attributes: inner}
case t.IsListType() && t.ListElementType() != nil:
return schema.List{Elem: TypeConstraint(*t.ListElementType())}
case t.IsMapType() && t.MapElementType() != nil:
return schema.Map{Elem: TypeConstraint(*t.MapElementType())}
case t.IsSetType() && t.SetElementType() != nil:
return schema.Set{Elem: TypeConstraint(*t.SetElementType())}
default:
return schema.Any{}
}
}
func collectionSchemaFromType(cType cty.Type) *schema.AttributeSchema {
var inferredSchema *schema.AttributeSchema
switch {
case cType.IsMapType() && cType.MapElementType() != nil:
inferredSchema = &schema.AttributeSchema{
Constraint: schema.Object{
Attributes: map[string]*schema.AttributeSchema{
"key": {Constraint: schema.String{}},
"value": {Constraint: TypeConstraint(*cType.MapElementType())},
},
},
}
case cType.IsListType() && cType.ListElementType() != nil:
inferredSchema = &schema.AttributeSchema{
Constraint: schema.Object{
Attributes: map[string]*schema.AttributeSchema{
"key": {Constraint: schema.Number{}},
"value": {Constraint: TypeConstraint(*cType.ListElementType())},
},
},
}
case cType.IsTupleType() && len(cType.TupleElementTypes()) == 1:
inferredSchema = &schema.AttributeSchema{
Constraint: schema.Object{
Attributes: map[string]*schema.AttributeSchema{
"key": {Constraint: schema.Number{}},
"value": {Constraint: TypeConstraint(cType.TupleElementTypes()[0])},
},
},
}
case cType.IsSetType() && cType.SetElementType() != nil:
cons := TypeConstraint(*cType.SetElementType())
inferredSchema = &schema.AttributeSchema{
Constraint: schema.Object{
Attributes: map[string]*schema.AttributeSchema{
"key": {Constraint: cons},
"value": {Constraint: cons},
},
},
}
}
return inferredSchema
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package lang
import (
"github.com/hashicorp/hcl/v2"
)
const (
NilCandidateKind CandidateKind = iota
// structural kinds
AttributeCandidateKind
BlockCandidateKind
LabelCandidateKind
// expressions
BoolCandidateKind
KeywordCandidateKind
ListCandidateKind
MapCandidateKind
NumberCandidateKind
ObjectCandidateKind
SetCandidateKind
StringCandidateKind
TupleCandidateKind
ReferenceCandidateKind
FunctionCandidateKind
)
//go:generate go run golang.org/x/tools/cmd/stringer -type=CandidateKind -output=candidate_kind_string.go
type CandidateKind uint
// Candidate represents a completion candidate in the form of
// an attribute, block, or a label
type Candidate struct {
Label string
Description MarkupContent
Detail string
IsDeprecated bool
TextEdit TextEdit
AdditionalTextEdits []TextEdit
Kind CandidateKind
// TriggerSuggest allows server to instruct the client whether
// to reopen candidate suggestion popup after insertion
TriggerSuggest bool
// ResolveHook allows resolution of additional information
// for a completion candidate via ResolveCandidate.
ResolveHook *ResolveHook
// SortText is an optional string that will be used when comparing this
// candidate with other candidates
SortText string
}
// TextEdit represents a change (edit) of an HCL config file
// in the form of a Snippet *and* NewText to replace the given Range.
//
// Snippet and NewText are equivalent, but NewText is provided
// for backwards-compatible reasons.
// Snippet uses 1-indexed placeholders, such as name = ${1:value}.
type TextEdit struct {
Range hcl.Range
NewText string
Snippet string
}
// Candidates represents a list of candidates and indication
// whether the list is complete or if it needs further filtering
// because there is too many candidates.
//
// Decoder has an upper limit for the number of candidates it can return
// and when the limit is reached, the list is considered incomplete.
type Candidates struct {
List []Candidate
IsComplete bool
}
func (ca Candidates) Len() int {
return len(ca.List)
}
func (ca Candidates) Less(i, j int) bool {
// TODO: sort by more metadata, such as IsRequired or IsDeprecated
return ca.List[i].Label < ca.List[j].Label
}
func (ca Candidates) Swap(i, j int) {
ca.List[i], ca.List[j] = ca.List[j], ca.List[i]
}
// NewCandidates creates a new (incomplete) list of candidates
// to be appended to.
func NewCandidates() Candidates {
return Candidates{
IsComplete: false,
}
}
// ZeroCandidates returns a (complete) "list" of no candidates
func ZeroCandidates() Candidates {
return Candidates{
IsComplete: true,
}
}
// CompleteCandidates creates a static (complete) list of candidates
//
// NewCandidates should be used at runtime instead, but CompleteCandidates
// provides a syntactic sugar useful in tests.
func CompleteCandidates(list []Candidate) Candidates {
return Candidates{
List: list,
IsComplete: true,
}
}
// IncompleteCandidates creates a static list of candidates
//
// NewCandidates should be used at runtime instead, but IncompleteCandidates
// provides a syntactic sugar useful in tests.
func IncompleteCandidates(list []Candidate) Candidates {
return Candidates{
List: list,
IsComplete: false,
}
}
// Code generated by "stringer -type=CandidateKind -output=candidate_kind_string.go"; DO NOT EDIT.
package lang
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[NilCandidateKind-0]
_ = x[AttributeCandidateKind-1]
_ = x[BlockCandidateKind-2]
_ = x[LabelCandidateKind-3]
_ = x[BoolCandidateKind-4]
_ = x[KeywordCandidateKind-5]
_ = x[ListCandidateKind-6]
_ = x[MapCandidateKind-7]
_ = x[NumberCandidateKind-8]
_ = x[ObjectCandidateKind-9]
_ = x[SetCandidateKind-10]
_ = x[StringCandidateKind-11]
_ = x[TupleCandidateKind-12]
_ = x[ReferenceCandidateKind-13]
_ = x[FunctionCandidateKind-14]
}
const _CandidateKind_name = "NilCandidateKindAttributeCandidateKindBlockCandidateKindLabelCandidateKindBoolCandidateKindKeywordCandidateKindListCandidateKindMapCandidateKindNumberCandidateKindObjectCandidateKindSetCandidateKindStringCandidateKindTupleCandidateKindReferenceCandidateKindFunctionCandidateKind"
var _CandidateKind_index = [...]uint16{0, 16, 38, 56, 74, 91, 111, 128, 144, 163, 182, 198, 217, 235, 257, 278}
func (i CandidateKind) String() string {
if i >= CandidateKind(len(_CandidateKind_index)-1) {
return "CandidateKind(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _CandidateKind_name[_CandidateKind_index[i]:_CandidateKind_index[i+1]]
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package lang
import (
"fmt"
"github.com/zclconf/go-cty/cty"
)
type CompletionHook struct {
Name string
}
type ResolveHook struct {
Name string `json:"resolve_hook,omitempty"`
Path string `json:"path,omitempty"`
}
type CompletionHooks []CompletionHook
func (chs CompletionHooks) Copy() CompletionHooks {
if chs == nil {
return nil
}
hooksCopy := make(CompletionHooks, len(chs))
copy(hooksCopy, chs)
return hooksCopy
}
// HookCandidate represents a completion candidate created and returned from a
// completion hook.
type HookCandidate struct {
// Label represents a human-readable name of the candidate
// if one exists (otherwise Value is used)
Label string
// Detail represents a human-readable string with additional
// information about this candidate, like symbol information.
Detail string
Kind CandidateKind
// Description represents human-readable description
// of the candidate
Description MarkupContent
// IsDeprecated indicates whether the candidate is deprecated
IsDeprecated bool
// RawInsertText represents the final text which is used to build the
// TextEdit for completion. It should contain quotes when completing
// strings.
RawInsertText string
// ResolveHook represents a resolve hook to call
// and any arguments to pass to it
ResolveHook *ResolveHook
// SortText is an optional string that will be used when comparing this
// candidate with other candidates
SortText string
}
// ExpressionCandidate is a simplified version of HookCandidate and the preferred
// way to create completion candidates from completion hooks for attributes
// values (expressions). One can use ExpressionCompletionCandidate to convert
// those into candidates.
type ExpressionCandidate struct {
// Value represents the value to be inserted
Value cty.Value
// Detail represents a human-readable string with additional
// information about this candidate, like symbol information.
Detail string
// Description represents human-readable description
// of the candidate
Description MarkupContent
// IsDeprecated indicates whether the candidate is deprecated
IsDeprecated bool
}
// ExpressionCompletionCandidate converts a simplified ExpressionCandidate
// into a HookCandidate while taking care of populating fields and quoting strings
func ExpressionCompletionCandidate(c ExpressionCandidate) HookCandidate {
// We're adding quotes to the string here, as we're always
// replacing the whole edit range for attribute expressions
text := fmt.Sprintf("%q", c.Value.AsString())
return HookCandidate{
Label: text,
Detail: c.Detail,
Kind: candidateKindForType(c.Value.Type()),
Description: c.Description,
IsDeprecated: c.IsDeprecated,
RawInsertText: text,
}
}
func candidateKindForType(t cty.Type) CandidateKind {
if t == cty.Bool {
return BoolCandidateKind
}
if t == cty.String {
return StringCandidateKind
}
if t == cty.Number {
return NumberCandidateKind
}
if t.IsListType() {
return ListCandidateKind
}
if t.IsSetType() {
return SetCandidateKind
}
if t.IsTupleType() {
return TupleCandidateKind
}
if t.IsMapType() {
return MapCandidateKind
}
if t.IsObjectType() {
return ObjectCandidateKind
}
return NilCandidateKind
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package lang
import "unique"
const (
NilKind MarkupKind = iota
PlainTextKind
MarkdownKind
)
//go:generate go run golang.org/x/tools/cmd/stringer -type=MarkupKind -output=markup_kind_string.go
type MarkupKind uint
// MarkupContent represents human-readable content
// which can be represented as Markdown or plaintext
// for backwards-compatible reasons.
type MarkupContent struct {
value *unique.Handle[string]
kind MarkupKind
}
func (m MarkupContent) Kind() MarkupKind {
return m.kind
}
func (m MarkupContent) Value() string {
if m.value == nil {
return ""
}
return m.value.Value()
}
func (m MarkupContent) AsDetail() string {
v := m.Value()
if v == "" {
return ""
}
return "\n\n" + v
}
func NewMarkup(kind MarkupKind, value string) MarkupContent {
h := unique.Make(value)
return MarkupContent{
kind: kind,
value: &h,
}
}
func PlainText(value string) MarkupContent {
return NewMarkup(PlainTextKind, value)
}
func Markdown(value string) MarkupContent {
return NewMarkup(MarkdownKind, value)
}
// Code generated by "stringer -type=MarkupKind -output=markup_kind_string.go"; DO NOT EDIT.
package lang
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[NilKind-0]
_ = x[PlainTextKind-1]
_ = x[MarkdownKind-2]
}
const _MarkupKind_name = "NilKindPlainTextKindMarkdownKind"
var _MarkupKind_index = [...]uint8{0, 7, 20, 32}
func (i MarkupKind) String() string {
if i >= MarkupKind(len(_MarkupKind_index)-1) {
return "MarkupKind(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _MarkupKind_name[_MarkupKind_index[i]:_MarkupKind_index[i+1]]
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package lang
type Path struct {
Path string
LanguageID string
}
func (path Path) Equals(p Path) bool {
return path.Path == p.Path && path.LanguageID == p.LanguageID
}
package semtok
type TokenModifier string
// Token modifiers predefined in the LSP spec
const (
TokenModifierDeclaration TokenModifier = "declaration"
TokenModifierDefinition TokenModifier = "definition"
TokenModifierReadonly TokenModifier = "readonly"
TokenModifierStatic TokenModifier = "static"
TokenModifierDeprecated TokenModifier = "deprecated"
TokenModifierAbstract TokenModifier = "abstract"
TokenModifierAsync TokenModifier = "async"
TokenModifierModification TokenModifier = "modification"
TokenModifierDocumentation TokenModifier = "documentation"
TokenModifierDefaultLibrary TokenModifier = "defaultLibrary"
)
type TokenModifiers []TokenModifier
func (tm TokenModifiers) AsStrings() []string {
modifiers := make([]string, len(tm))
for i, tokenModifier := range tm {
modifiers[i] = string(tokenModifier)
}
return modifiers
}
package semtok
type TokenType string
// Token types predefined in the LSP spec
const (
TokenTypeClass TokenType = "class"
TokenTypeComment TokenType = "comment"
TokenTypeEnum TokenType = "enum"
TokenTypeEnumMember TokenType = "enumMember"
TokenTypeEvent TokenType = "event"
TokenTypeFunction TokenType = "function"
TokenTypeInterface TokenType = "interface"
TokenTypeKeyword TokenType = "keyword"
TokenTypeMacro TokenType = "macro"
TokenTypeMethod TokenType = "method"
TokenTypeModifier TokenType = "modifier"
TokenTypeNamespace TokenType = "namespace"
TokenTypeNumber TokenType = "number"
TokenTypeOperator TokenType = "operator"
TokenTypeParameter TokenType = "parameter"
TokenTypeProperty TokenType = "property"
TokenTypeRegexp TokenType = "regexp"
TokenTypeString TokenType = "string"
TokenTypeStruct TokenType = "struct"
TokenTypeType TokenType = "type"
TokenTypeTypeParameter TokenType = "typeParameter"
TokenTypeVariable TokenType = "variable"
)
type TokenTypes []TokenType
func (tt TokenTypes) AsStrings() []string {
types := make([]string, len(tt))
for i, tokenType := range tt {
types[i] = string(tokenType)
}
return types
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package lang
import (
"github.com/zclconf/go-cty/cty"
)
type exprKindSigil struct{}
type SymbolExprKind interface {
isSymbolExprKindSigil() exprKindSigil
}
type LiteralTypeKind struct {
Type cty.Type
}
func (LiteralTypeKind) isSymbolExprKindSigil() exprKindSigil {
return exprKindSigil{}
}
type TupleConsExprKind struct{}
func (TupleConsExprKind) isSymbolExprKindSigil() exprKindSigil {
return exprKindSigil{}
}
type ObjectConsExprKind struct{}
func (ObjectConsExprKind) isSymbolExprKindSigil() exprKindSigil {
return exprKindSigil{}
}
type ReferenceExprKind struct{}
func (ReferenceExprKind) isSymbolExprKindSigil() exprKindSigil {
return exprKindSigil{}
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package lang
import (
"github.com/hashicorp/hcl/v2"
)
type DiagnosticsMap map[string]hcl.Diagnostics
func (dm DiagnosticsMap) Extend(diagMap DiagnosticsMap) DiagnosticsMap {
for fileName, diags := range diagMap {
_, ok := dm[fileName]
if !ok {
dm[fileName] = make(hcl.Diagnostics, 0)
}
dm[fileName] = dm[fileName].Extend(diags)
}
return dm
}
// Count returns the number of diagnostics for all files
func (dm DiagnosticsMap) Count() int {
count := 0
for _, diags := range dm {
count += len(diags)
}
return count
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package schema
import (
"errors"
"fmt"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/lang"
)
// AttributeSchema describes schema for an attribute
type AttributeSchema struct {
Description lang.MarkupContent
IsRequired bool
IsOptional bool
// Constraint represents expression constraint e.g. what types of
// expressions are expected for the attribute
//
// Constraints are immutable after construction by convention. It is
// particularly important not to mutate a constraint after it has been
// added to an AttributeSchema.
Constraint Constraint
// CompletionHooks represent any hooks which provide
// additional completion candidates for the attribute.
// These are typically candidates which cannot be provided
// via schema and come from external APIs or other sources.
CompletionHooks lang.CompletionHooks
}
func (as *AttributeSchema) Validate() error {
if as.IsOptional && as.IsRequired {
return errors.New("IsOptional or IsRequired must be set, not both")
}
if !as.IsRequired && !as.IsOptional {
return errors.New("one of IsRequired or IsOptional must be set")
}
if con, ok := as.Constraint.(Validatable); ok {
err := con.Validate()
if err != nil {
return fmt.Errorf("constraint: %T: %s", as.Constraint, err)
}
}
return nil
}
func (as *AttributeSchema) Copy() *AttributeSchema {
if as == nil {
return nil
}
newAs := &AttributeSchema{
IsRequired: as.IsRequired,
IsOptional: as.IsOptional,
Description: as.Description,
CompletionHooks: as.CompletionHooks.Copy(),
// We do not copy Constraint as it should be immutable
Constraint: as.Constraint,
}
return newAs
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package schema
import (
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/lang"
)
// BasicBlockSchema provides a block description and associated label schemas.
type BasicBlockSchema struct {
Description lang.MarkupContent
Labels []*LabelSchema
AllowMultiple bool
}
func (b *BasicBlockSchema) Copy() *BasicBlockSchema {
if b == nil {
return nil
}
newBs := &BasicBlockSchema{
Description: b.Description,
}
if len(b.Labels) > 0 {
newBs.Labels = make([]*LabelSchema, len(b.Labels))
for i, l := range b.Labels {
newBs.Labels[i] = l.Copy()
}
}
return newBs
}
// BodySchema describes the schema for a block
type BodySchema struct {
Description lang.MarkupContent
Attributes map[string]*AttributeSchema
NestedBlocks map[string]*BasicBlockSchema
}
func (bs *BodySchema) Copy() *BodySchema {
if bs == nil {
return nil
}
newBs := &BodySchema{
Description: bs.Description,
}
if bs.NestedBlocks != nil {
newBs.NestedBlocks = make(map[string]*BasicBlockSchema, len(bs.NestedBlocks))
for k, v := range bs.NestedBlocks {
newBs.NestedBlocks[k] = v.Copy()
}
}
if bs.Attributes != nil {
newBs.Attributes = make(map[string]*AttributeSchema, len(bs.Attributes))
for k, v := range bs.Attributes {
newBs.Attributes[k] = v
}
}
return newBs
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package schema
import (
"github.com/zclconf/go-cty/cty"
)
// Any represents an unknown type that can be satisfied by any value.
type Any struct{}
func (Any) isConstraintImpl() constraintSigil { return constraintSigil{} }
func (ae Any) FriendlyName() string { return cty.DynamicPseudoType.FriendlyNameForConstraint() }
func (ae Any) Copy() Constraint { return Any{} }
func (ae Any) ConstraintType() (cty.Type, bool) { return cty.DynamicPseudoType, true }
func (ae Any) EmptyCompletionData(nextPlaceholder int, nestingLevel int) CompletionData {
return noCompletion
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package schema
import (
"fmt"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/lang"
"github.com/zclconf/go-cty/cty"
)
// List represents a list, equivalent of a TupleConsExpr
// interpreted as list, i.e. ordering of item (which are all
// the same type) matters.
type List struct {
// Elem defines constraint to apply to each item
Elem Constraint
// Description defines description of the whole list (affects hover)
Description lang.MarkupContent
// MinItems defines minimum number of items (affects completion)
MinItems uint64
// MaxItems defines maximum number of items (affects completion)
MaxItems uint64
}
func (List) isConstraintImpl() constraintSigil {
return constraintSigil{}
}
func (l List) FriendlyName() string {
if l.Elem != nil && l.Elem.FriendlyName() != "" {
return fmt.Sprintf("list of %s", l.Elem.FriendlyName())
}
return "list"
}
func (l List) Copy() Constraint {
var elem Constraint
if l.Elem != nil {
elem = l.Elem.Copy()
}
return List{
Elem: elem,
Description: l.Description,
MinItems: l.MinItems,
MaxItems: l.MaxItems,
}
}
func (l List) EmptyCompletionData(nextPlaceholder int, nestingLevel int) CompletionData {
if l.Elem == nil {
return CompletionData{
NewText: "[ ]",
Snippet: fmt.Sprintf("[ ${%d} ]", nextPlaceholder),
NextPlaceholder: nextPlaceholder + 1,
}
}
elemData := l.Elem.EmptyCompletionData(nextPlaceholder, nestingLevel)
if elemData.NewText == "" || elemData.Snippet == "" {
return CompletionData{
NewText: "[ ]",
Snippet: fmt.Sprintf("[ ${%d} ]", nextPlaceholder),
TriggerSuggest: elemData.TriggerSuggest,
NextPlaceholder: nextPlaceholder + 1,
}
}
return CompletionData{
NewText: fmt.Sprintf("[ %s ]", elemData.NewText),
Snippet: fmt.Sprintf("[ %s ]", elemData.Snippet),
NextPlaceholder: elemData.NextPlaceholder,
}
}
func (l List) ConstraintType() (cty.Type, bool) {
if l.Elem == nil {
return cty.NilType, false
}
elemCons, ok := l.Elem.(TypeAwareConstraint)
if !ok {
return cty.NilType, false
}
elemType, ok := elemCons.ConstraintType()
if !ok {
return cty.NilType, false
}
return cty.List(elemType), true
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package schema
import (
"errors"
"fmt"
"github.com/zclconf/go-cty/cty"
)
// LiteralType represents literal type constraint
// e.g. any literal string ("foo"), number (42), etc.
//
// Non-literal expressions (even if these evaluate to the given
// type) are excluded.
//
// Complex types are supported, but dedicated List,
// Set, Map and other types are preferred, as these can
// convey more details, such as description, unlike
// e.g. LiteralType{Type: cty.List(...)}.
type LiteralType struct {
Type cty.Type
}
func (LiteralType) isConstraintImpl() constraintSigil {
return constraintSigil{}
}
func (lt LiteralType) FriendlyName() string {
return lt.Type.FriendlyNameForConstraint()
}
func (lt LiteralType) Copy() Constraint {
return LiteralType{
Type: lt.Type,
}
}
func (lt LiteralType) Validate() error {
if lt.Type == cty.NilType {
return errors.New("expected Type not to be nil")
}
return nil
}
func (lt LiteralType) EmptyCompletionData(nextPlaceholder int, nestingLevel int) CompletionData {
if lt.Type.IsPrimitiveType() {
var newText, snippet string
switch lt.Type {
case cty.Bool:
newText = fmt.Sprintf("%t", false)
// TODO: consider using snippet "choice"
// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#snippet_syntax
snippet = fmt.Sprintf("${%d:false}", nextPlaceholder)
case cty.String:
newText = fmt.Sprintf("%q", "value")
snippet = fmt.Sprintf("\"${%d:%s}\"", nextPlaceholder, "value")
case cty.Number:
newText = "0"
snippet = fmt.Sprintf("${%d:0}", nextPlaceholder)
}
nextPlaceholder++
return CompletionData{
NewText: newText,
Snippet: snippet,
NextPlaceholder: nextPlaceholder,
}
}
if lt.Type.IsListType() {
listCons := List{
Elem: LiteralType{
Type: lt.Type.ElementType(),
},
}
return listCons.EmptyCompletionData(nextPlaceholder, nestingLevel)
}
if lt.Type.IsSetType() {
setCons := Set{
Elem: LiteralType{
Type: lt.Type.ElementType(),
},
}
return setCons.EmptyCompletionData(nextPlaceholder, nestingLevel)
}
if lt.Type.IsMapType() {
mapCons := Map{
Elem: LiteralType{
Type: lt.Type.ElementType(),
},
}
return mapCons.EmptyCompletionData(nextPlaceholder, nestingLevel)
}
if lt.Type.IsObjectType() {
attrTypes := lt.Type.AttributeTypes()
attrs := ObjectAttributes{}
for name, attrType := range attrTypes {
aSchema := &AttributeSchema{
Constraint: LiteralType{
Type: attrType,
},
}
if lt.Type.AttributeOptional(name) {
aSchema.IsOptional = true
} else {
aSchema.IsRequired = true
}
attrs[name] = aSchema
}
cons := Object{
Attributes: attrs,
}
return cons.EmptyCompletionData(nextPlaceholder, nestingLevel)
}
return CompletionData{
NextPlaceholder: nextPlaceholder,
}
}
func (lt LiteralType) ConstraintType() (cty.Type, bool) {
return lt.Type, true
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package schema
import (
"fmt"
"strings"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/lang"
"github.com/zclconf/go-cty/cty"
)
// Map represents a map, equivalent of hclsyntax.ObjectConsExpr
// interpreted as map, i.e. with items of unknown keys
// and same value types.
type Map struct {
// Elem defines constraint to apply to each item of the map
Elem Constraint
// Name overrides friendly name of the constraint
Name string
// Description defines description of the whole map (affects hover)
Description lang.MarkupContent
// MinItems defines minimum number of items (affects completion)
MinItems uint64
// MaxItems defines maximum number of items (affects completion)
MaxItems uint64
// AllowInterpolatedKeys determines whether the key names can be
// interpolated (true) or static (literal strings only).
AllowInterpolatedKeys bool
}
func (Map) isConstraintImpl() constraintSigil {
return constraintSigil{}
}
func (m Map) FriendlyName() string {
if m.Name == "" {
if m.Elem != nil && m.Elem.FriendlyName() != "" {
return fmt.Sprintf("map of %s", m.Elem.FriendlyName())
}
return "map"
}
return m.Name
}
func (m Map) Copy() Constraint {
var elem Constraint
if m.Elem != nil {
elem = m.Elem.Copy()
}
return Map{
Elem: elem,
Name: m.Name,
Description: m.Description,
MinItems: m.MinItems,
MaxItems: m.MaxItems,
AllowInterpolatedKeys: m.AllowInterpolatedKeys,
}
}
func (m Map) EmptyCompletionData(nextPlaceholder int, nestingLevel int) CompletionData {
rootNesting := strings.Repeat(" ", nestingLevel)
insideNesting := strings.Repeat(" ", nestingLevel+1)
if m.Elem == nil {
return CompletionData{
NewText: fmt.Sprintf("{\n%s\n%s}", insideNesting, rootNesting),
Snippet: fmt.Sprintf("{\n%s${%d}\n%s}", insideNesting, nextPlaceholder, rootNesting),
NextPlaceholder: nextPlaceholder + 1,
}
}
elemData := m.Elem.EmptyCompletionData(nextPlaceholder+1, nestingLevel+1)
if elemData.NewText == "" || elemData.Snippet == "" {
return CompletionData{
NewText: fmt.Sprintf("{\n%s\n%s}", insideNesting, rootNesting),
Snippet: fmt.Sprintf("{\n%s${%d}\n%s}", insideNesting, nextPlaceholder, rootNesting),
NextPlaceholder: nextPlaceholder + 1,
TriggerSuggest: elemData.TriggerSuggest,
}
}
return CompletionData{
NewText: fmt.Sprintf("{\n%s\"name\" = %s\n%s}", insideNesting, elemData.NewText, rootNesting),
Snippet: fmt.Sprintf("{\n%s\"${%d:name}\" = %s\n%s}", insideNesting, nextPlaceholder, elemData.Snippet, rootNesting),
NextPlaceholder: elemData.NextPlaceholder,
TriggerSuggest: elemData.TriggerSuggest,
}
}
func (m Map) ConstraintType() (cty.Type, bool) {
if m.Elem == nil {
return cty.NilType, false
}
elemCons, ok := m.Elem.(TypeAwareConstraint)
if !ok {
return cty.NilType, false
}
elemType, ok := elemCons.ConstraintType()
if !ok {
return cty.NilType, false
}
return cty.Map(elemType), true
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package schema
import (
"fmt"
"sort"
"strings"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/lang"
"github.com/zclconf/go-cty/cty"
)
// Object represents an object, equivalent of hclsyntax.ObjectConsExpr
// interpreted as object, i.e. with items of known keys
// and different value types.
type Object struct {
Name string // overrides friendly name of the constraint
Attributes ObjectAttributes // names and constraints of attributes within the object
Description lang.MarkupContent // description of the whole object (affects hover)
AllowInterpolatedKeys bool // determines whether the attribute names can be interpolated
AnyAttribute Constraint // determines if we allow unknown attributes of this type
PrefillRequiredKeys bool // prefill any required keys for the object
}
type ObjectAttributes map[string]*AttributeSchema
func (Object) isConstraintImpl() constraintSigil {
return constraintSigil{}
}
func (o Object) FriendlyName() string {
if o.Name == "" {
return "object"
}
return o.Name
}
func (o Object) Copy() Constraint {
return Object{
Attributes: o.Attributes.Copy(),
Name: o.Name,
Description: o.Description,
AllowInterpolatedKeys: o.AllowInterpolatedKeys,
PrefillRequiredKeys: o.PrefillRequiredKeys,
}
}
func (o Object) EmptyCompletionData(placeholder int, nestingLevel int) CompletionData {
nesting := strings.Repeat(" ", nestingLevel)
attrNesting := strings.Repeat(" ", nestingLevel+1)
triggerSuggest := len(o.Attributes) > 0
emptyObjectData := CompletionData{
NewText: fmt.Sprintf("{\n%s\n%s}", attrNesting, nesting),
Snippet: fmt.Sprintf("{\n%s${%d}\n%s}", attrNesting, placeholder, nesting),
NextPlaceholder: placeholder + 1,
TriggerSuggest: triggerSuggest,
}
if !o.PrefillRequiredKeys {
return emptyObjectData
}
attrData, ok := o.attributesCompletionData(placeholder, nestingLevel)
if !ok {
return emptyObjectData
}
return CompletionData{
NewText: fmt.Sprintf("{\n%s%s}", attrData.NewText, nesting),
Snippet: fmt.Sprintf("{\n%s%s}", attrData.Snippet, nesting),
NextPlaceholder: attrData.NextPlaceholder,
}
}
func (o Object) attributesCompletionData(placeholder, nestingLevel int) (CompletionData, bool) {
newText, snippet := "", ""
anyRequiredFields := false
attrNesting := strings.Repeat(" ", nestingLevel+1)
nextPlaceholder := placeholder
attrNames := sortedObjectExprAttrNames(o.Attributes)
for _, name := range attrNames {
attr := o.Attributes[name]
attrData := attr.Constraint.EmptyCompletionData(nextPlaceholder, nestingLevel+1)
if attrData.NewText == "" || attrData.Snippet == "" {
return CompletionData{}, false
}
if attr.IsRequired {
anyRequiredFields = true
} else {
continue
}
newText += fmt.Sprintf("%s%s = %s\n", attrNesting, name, attrData.NewText)
snippet += fmt.Sprintf("%s%s = %s\n", attrNesting, name, attrData.Snippet)
nextPlaceholder = attrData.NextPlaceholder
}
if anyRequiredFields {
return CompletionData{
NewText: newText,
Snippet: snippet,
NextPlaceholder: nextPlaceholder,
}, true
}
return CompletionData{}, false
}
func sortedObjectExprAttrNames(attributes ObjectAttributes) []string {
if len(attributes) == 0 {
return []string{}
}
constraints := attributes
names := make([]string, len(constraints))
i := 0
for name := range constraints {
names[i] = name
i++
}
sort.Strings(names)
return names
}
func (o Object) ConstraintType() (cty.Type, bool) {
objAttributes := make(map[string]cty.Type)
for name, attr := range o.Attributes {
cons, ok := attr.Constraint.(TypeAwareConstraint)
if !ok {
return cty.NilType, false
}
attrType, ok := cons.ConstraintType()
if !ok {
return cty.NilType, false
}
objAttributes[name] = attrType
}
return cty.Object(objAttributes), true
}
func (oa ObjectAttributes) Copy() ObjectAttributes {
m := ObjectAttributes{}
for name, aSchema := range oa {
m[name] = aSchema.Copy()
}
return m
}
package schema
import "github.com/zclconf/go-cty/cty"
// String represents a string type.
type String struct{}
func (String) isConstraintImpl() constraintSigil { return constraintSigil{} }
func (s String) FriendlyName() string { return cty.String.FriendlyNameForConstraint() }
func (s String) Copy() Constraint { return String{} }
func (s String) ConstraintType() (cty.Type, bool) { return cty.String, true }
func (s String) EmptyCompletionData(nextPlaceholder int, nestingLevel int) CompletionData {
return noCompletion
}
// Number represents a number type.
type Number struct{}
func (Number) isConstraintImpl() constraintSigil { return constraintSigil{} }
func (n Number) FriendlyName() string { return cty.Number.FriendlyNameForConstraint() }
func (n Number) Copy() Constraint { return Number{} }
func (n Number) ConstraintType() (cty.Type, bool) { return cty.Number, true }
func (n Number) EmptyCompletionData(nextPlaceholder int, nestingLevel int) CompletionData {
return noCompletion
}
// Bool represents a boolean type.
type Bool struct{}
func (Bool) isConstraintImpl() constraintSigil { return constraintSigil{} }
func (b Bool) FriendlyName() string { return cty.Bool.FriendlyNameForConstraint() }
func (b Bool) Copy() Constraint { return Bool{} }
func (b Bool) ConstraintType() (cty.Type, bool) { return cty.Bool, true }
func (b Bool) EmptyCompletionData(nextPlaceholder int, nestingLevel int) CompletionData {
return noCompletion
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package schema
import (
"fmt"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/lang"
"github.com/zclconf/go-cty/cty"
)
// Set represents a set, equivalent of hclsyntax.TupleConsExpr
// interpreted as set, i.e. ordering of items (which are
// of the same type) does not matter.
type Set struct {
// Elem defines constraint to apply to each item
Elem Constraint
// Description defines description of the whole list (affects hover)
Description lang.MarkupContent
// MinItems defines minimum number of items (affects completion)
MinItems uint64
// MaxItems defines maximum number of items (affects completion)
MaxItems uint64
}
func (Set) isConstraintImpl() constraintSigil {
return constraintSigil{}
}
func (s Set) FriendlyName() string {
if s.Elem != nil && s.Elem.FriendlyName() != "" {
return fmt.Sprintf("set of %s", s.Elem.FriendlyName())
}
return "set"
}
func (s Set) Copy() Constraint {
var elem Constraint
if s.Elem != nil {
elem = s.Elem.Copy()
}
return Set{
Elem: elem,
Description: s.Description,
MinItems: s.MinItems,
MaxItems: s.MaxItems,
}
}
func (s Set) EmptyCompletionData(nextPlaceholder int, nestingLevel int) CompletionData {
if s.Elem == nil {
return CompletionData{
NewText: "[ ]",
Snippet: fmt.Sprintf("[ ${%d} ]", nextPlaceholder),
NextPlaceholder: nextPlaceholder + 1,
}
}
elemData := s.Elem.EmptyCompletionData(nextPlaceholder, nestingLevel)
if elemData.NewText == "" || elemData.Snippet == "" {
return CompletionData{
NewText: "[ ]",
Snippet: fmt.Sprintf("[ ${%d} ]", nextPlaceholder),
TriggerSuggest: elemData.TriggerSuggest,
NextPlaceholder: nextPlaceholder + 1,
}
}
return CompletionData{
NewText: fmt.Sprintf("[ %s ]", elemData.NewText),
Snippet: fmt.Sprintf("[ %s ]", elemData.Snippet),
NextPlaceholder: elemData.NextPlaceholder,
}
}
func (s Set) ConstraintType() (cty.Type, bool) {
if s.Elem == nil {
return cty.NilType, false
}
elemCons, ok := s.Elem.(TypeAwareConstraint)
if !ok {
return cty.NilType, false
}
elemType, ok := elemCons.ConstraintType()
if !ok {
return cty.NilType, false
}
return cty.Set(elemType), true
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package schema
// TypeDeclaration represents a type declaration as
// interpreted by HCL's ext/typeexpr package,
// i.e. declaration of cty.Type in HCL
type TypeDeclaration struct {
// TODO: optional object attribute mode
}
func (TypeDeclaration) isConstraintImpl() constraintSigil {
return constraintSigil{}
}
func (td TypeDeclaration) FriendlyName() string {
return "type"
}
func (td TypeDeclaration) Copy() Constraint {
return TypeDeclaration{}
}
func (td TypeDeclaration) EmptyCompletionData(nextPlaceholder int, nestingLevel int) CompletionData {
return CompletionData{
TriggerSuggest: true,
NextPlaceholder: nextPlaceholder,
}
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package schema
import (
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/lang"
)
// LabelSchema describes schema for a label on a particular position
type LabelSchema struct {
Name string
Description lang.MarkupContent
AllowedValues []string
}
func (ls *LabelSchema) CanComplete() bool {
return len(ls.AllowedValues) > 0
}
func (ls *LabelSchema) Copy() *LabelSchema {
if ls == nil {
return nil
}
var v []string
if ls.AllowedValues != nil {
v = make([]string, len(ls.AllowedValues))
copy(v, ls.AllowedValues)
}
return &LabelSchema{
Name: ls.Name,
Description: ls.Description,
AllowedValues: v,
}
}
package schema
import (
"log"
"github.com/hashicorp/hcl/v2/hclsyntax"
)
var sentinel = &hclsyntax.Block{}
type blockStack struct {
blocks []*hclsyntax.Block
}
func (s *blockStack) HasAncestorOfType(t string) bool {
for _, block := range s.blocks {
if block.Type == t {
return true
}
}
return false
}
func (s *blockStack) Push(block *hclsyntax.Block) {
s.blocks = append(s.blocks, block)
}
func (s *blockStack) Pop() {
if len(s.blocks) == 0 {
log.Println("error: pop stack at 0 depth")
return
}
s.blocks = s.blocks[:len(s.blocks)-1]
}
func (s *blockStack) Peek(n int) *hclsyntax.Block {
index := len(s.blocks) - 1 - n
if index < 0 {
return sentinel
}
return s.blocks[index]
}
// BlockStack provides a mechanism to track the current block structure that is seen
// in LIFO manner. Peek and Pop never panic and always return an empty block for
// out of bounds calls.
type BlockStack interface {
Push(*hclsyntax.Block)
Peek(n int) *hclsyntax.Block
Pop()
HasAncestorOfType(t string) bool
}
func NewBlockStack() BlockStack {
return &blockStack{}
}
// Lookup provides a mechanism to provide just-in-time schema information for
// an entity at a specific position.
type Lookup interface {
// BodySchema returns the body schema at the current block position.
BodySchema(bs BlockStack) *BodySchema
// LabelSchema returns the label schema at the current block position.
LabelSchema(bs BlockStack) []*LabelSchema
// AttributeSchema returns the attribute schema at the current block and attribute position.
AttributeSchema(bs BlockStack, attrName string) *AttributeSchema
// Functions returns available function signatures keyed by function name.
Functions() map[string]FunctionSignature
// ImpliedAttributeSchema returns a computed schema implied by the attribute expression.
// This is typically called when hovering over the name of an attribute.
ImpliedAttributeSchema(bs BlockStack, attrName string) *AttributeSchema
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package schema
import (
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/function"
)
type FunctionSignature struct {
// Description is an optional human-readable description
// of the function.
Description string
// Detail is additional detail that can be used in hover descriptions.
Detail string
// ReturnType is the ctyjson representation of the function's
// return types based on supplying all parameters using
// dynamic types. Functions can have dynamic return types.
ReturnType cty.Type
// Params describes the function's fixed positional parameters.
Params []function.Parameter
// VarParam describes the function's variadic
// parameter if it is supported.
VarParam *function.Parameter
}
func (fs *FunctionSignature) Copy() *FunctionSignature {
newFS := &FunctionSignature{
Description: fs.Description,
Detail: fs.Detail,
ReturnType: fs.ReturnType,
}
newFS.Params = make([]function.Parameter, len(fs.Params))
copy(newFS.Params, fs.Params)
if fs.VarParam != nil {
vpCpy := *fs.VarParam
newFS.VarParam = &vpCpy
}
return newFS
}
// Package writer returns source code for HCL syntax nodes, for debugging use.
package writer
import (
"fmt"
"reflect"
"regexp"
"sort"
"strconv"
"strings"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/zclconf/go-cty/cty"
)
// NodeToSource returns the source representation of the supplied syntax node.
func NodeToSource(n hclsyntax.Node) string {
var w writer
if expr, ok := n.(hclsyntax.Expression); ok {
w.Expr(expr)
} else {
switch n := n.(type) {
case *hclsyntax.Body:
w.body(n)
case *hclsyntax.Block:
w.block(n)
case *hclsyntax.Attribute:
w.attribute(n)
default:
panic(fmt.Sprintf("unknown node type: %T", n))
}
}
return w.result.String()
}
type writer struct {
result strings.Builder
canonical bool
}
// Expr unpacks the struct implementing the expression and dispatches to
// the appropriate walker method.
func (w *writer) Expr(expr hclsyntax.Expression) {
switch e := expr.(type) {
case *hclsyntax.ExprSyntaxError:
w.write("✗")
case *hclsyntax.LiteralValueExpr:
w.literalValue(e)
case *hclsyntax.ScopeTraversalExpr:
w.scopeTraversal(e)
case *hclsyntax.RelativeTraversalExpr:
w.relativeTraversal(e)
case *hclsyntax.FunctionCallExpr:
w.functionCall(e)
case *hclsyntax.ConditionalExpr:
w.conditional(e)
case *hclsyntax.BinaryOpExpr:
w.binaryOp(e)
case *hclsyntax.UnaryOpExpr:
w.unaryOp(e)
case *hclsyntax.ObjectConsExpr:
w.objectCons(e)
case *hclsyntax.ObjectConsKeyExpr:
w.objectConsKey(e)
case *hclsyntax.TupleConsExpr:
w.tupleCons(e)
case *hclsyntax.TemplateExpr:
w.template(e)
case *hclsyntax.TemplateWrapExpr:
w.templateWrap(e)
case *hclsyntax.IndexExpr:
w.index(e)
case *hclsyntax.SplatExpr:
w.splat(e)
case *hclsyntax.ForExpr:
w.forExpr(e)
case *hclsyntax.ParenthesesExpr:
w.parentheses(e)
case *hclsyntax.AnonSymbolExpr:
w.anonSymbol(e)
default:
w.write("<unexpected expression type ")
w.write(reflect.TypeOf(expr).String())
w.write(">")
}
}
type writeable struct {
attr *hclsyntax.Attribute
block *hclsyntax.Block
}
func (w writeable) isAttribute() bool {
return w.attr != nil
}
func (w writeable) Range() hcl.Range {
if w.isAttribute() {
return w.attr.Range()
}
return w.block.Range()
}
func newAttrW(attr *hclsyntax.Attribute) writeable {
return writeable{attr: attr}
}
func newBlockW(block *hclsyntax.Block) writeable {
return writeable{block: block}
}
func (w *writer) body(body *hclsyntax.Body) {
var ws []writeable
for _, attr := range body.Attributes {
ws = append(ws, newAttrW(attr))
}
for _, block := range body.Blocks {
ws = append(ws, newBlockW(block))
}
canonSort := func(left, right writeable) bool {
if left.isAttribute() && right.isAttribute() {
return left.attr.Name < right.attr.Name
}
if !left.isAttribute() && !right.isAttribute() {
return left.block.Type < right.block.Type
}
if left.isAttribute() {
return true
}
return false
}
sort.Slice(ws, func(i, j int) bool {
left := ws[i]
right := ws[j]
if w.canonical {
return canonSort(left, right)
} else {
lr := left.Range()
rr := right.Range()
if lr.Start.Line == rr.Start.Line {
return canonSort(left, right)
}
return lr.Start.Line < rr.Start.Line
}
})
count := 0
for _, wk := range ws {
if wk.isAttribute() {
w.attribute(wk.attr)
} else {
if count > 0 {
w.write("\n")
}
w.block(wk.block)
}
count++
}
}
func (w *writer) block(block *hclsyntax.Block) {
w.write(block.Type)
for _, label := range block.Labels {
w.write(" ")
w.write(unwrapIdent(strconv.Quote(label)))
}
w.write(" {\n")
bodyContent := NodeToSource(block.Body)
if bodyContent != "" {
lines := strings.Split(strings.TrimRight(bodyContent, "\n"), "\n")
for _, line := range lines {
if line != "" {
w.write(" ")
w.write(line)
}
w.write("\n")
}
}
w.write("}\n")
}
func (w *writer) attribute(attr *hclsyntax.Attribute) {
w.write(attr.Name)
w.write(" = ")
w.Expr(attr.Expr)
w.write("\n")
}
func (w *writer) write(s string) {
w.result.WriteString(s)
}
func (w *writer) literalValue(e *hclsyntax.LiteralValueExpr) {
w.write(literalValueToHCL(e.Val))
}
func (w *writer) scopeTraversal(e *hclsyntax.ScopeTraversalExpr) {
traversal := e.Traversal
for _, step := range traversal {
switch s := step.(type) {
case hcl.TraverseRoot:
w.write(s.Name)
case hcl.TraverseAttr:
w.write(".")
w.write(s.Name)
case hcl.TraverseIndex:
key := literalValueToHCL(s.Key)
if IsQuotedIdentifier(key) {
w.write(".")
w.write(unwrapIdent(key))
} else {
w.write("[")
w.write(key)
w.write("]")
}
}
}
}
func (w *writer) relativeTraversal(e *hclsyntax.RelativeTraversalExpr) {
w.Expr(e.Source)
for _, step := range e.Traversal {
switch s := step.(type) {
case hcl.TraverseAttr:
w.write(".")
w.write(s.Name)
case hcl.TraverseIndex:
key := literalValueToHCL(s.Key)
w.write("[")
w.write(key)
w.write("]")
}
}
}
func (w *writer) index(e *hclsyntax.IndexExpr) {
w.Expr(e.Collection)
w.write("[")
w.Expr(e.Key)
w.write("]")
}
func (w *writer) binaryOp(e *hclsyntax.BinaryOpExpr) {
w.Expr(e.LHS)
w.write(" ")
w.write(operationToHCL(e.Op))
w.write(" ")
w.Expr(e.RHS)
}
func (w *writer) unaryOp(e *hclsyntax.UnaryOpExpr) {
w.write(operationToHCL(e.Op))
w.Expr(e.Val)
}
func (w *writer) conditional(e *hclsyntax.ConditionalExpr) {
w.Expr(e.Condition)
w.write(" ? ")
w.Expr(e.TrueResult)
w.write(" : ")
w.Expr(e.FalseResult)
}
func (w *writer) functionCall(e *hclsyntax.FunctionCallExpr) {
w.write(e.Name)
w.write("(")
for i, arg := range e.Args {
if i > 0 {
w.write(", ")
}
w.Expr(arg)
}
w.write(")")
}
func (w *writer) tupleCons(e *hclsyntax.TupleConsExpr) {
w.write("[")
for i, elem := range e.Exprs {
if i > 0 {
w.write(", ")
}
w.Expr(elem)
}
w.write("]")
}
func (w *writer) objectCons(e *hclsyntax.ObjectConsExpr) {
w.write("{\n")
for _, item := range e.Items {
w.write(unwrapIdent(NodeToSource(item.KeyExpr)))
w.write(" = ")
w.Expr(item.ValueExpr)
w.write("\n")
}
w.write("}")
}
func (w *writer) objectConsKey(e *hclsyntax.ObjectConsKeyExpr) {
w.Expr(e.Wrapped)
}
func (w *writer) template(e *hclsyntax.TemplateExpr) {
w.write(templateExprToHCL(e))
}
func (w *writer) templateWrap(e *hclsyntax.TemplateWrapExpr) {
w.Expr(e.Wrapped)
}
func (w *writer) parentheses(e *hclsyntax.ParenthesesExpr) {
w.write("(")
w.Expr(e.Expression)
w.write(")")
}
func (w *writer) forExpr(e *hclsyntax.ForExpr) {
// start bracket - [ for tuple, { for object
if e.KeyExpr != nil {
w.write("{")
} else {
w.write("[")
}
w.write("for ")
// key variable (optional)
if e.KeyVar != "" {
w.write(e.KeyVar)
w.write(", ")
}
// value variable
w.write(e.ValVar)
w.write(" in ")
w.Expr(e.CollExpr)
w.write(" : ")
// key expression (for objects)
if e.KeyExpr != nil {
w.Expr(e.KeyExpr)
w.write(" => ")
}
// value expression
w.Expr(e.ValExpr)
// condition (optional)
if e.CondExpr != nil {
w.write(" if ")
w.Expr(e.CondExpr)
}
// group operator (optional)
if e.Group {
w.write("...")
}
// end bracket
if e.KeyExpr != nil {
w.write("}")
} else {
w.write("]")
}
}
func (w *writer) splat(e *hclsyntax.SplatExpr) {
w.Expr(e.Source)
w.write("[*]")
w.Expr(e.Each)
}
func (w *writer) anonSymbol(_ *hclsyntax.AnonSymbolExpr) {
}
// templateExprToHCL handles template expressions (strings with interpolation)
func templateExprToHCL(expr *hclsyntax.TemplateExpr) string {
if len(expr.Parts) == 0 {
return `""`
}
if len(expr.Parts) == 1 {
if lit, ok := expr.Parts[0].(*hclsyntax.LiteralValueExpr); ok && lit.Val.Type() == cty.String {
return strconv.Quote(lit.Val.AsString())
}
}
var result strings.Builder
result.WriteString(`"`)
for _, part := range expr.Parts {
switch p := part.(type) {
case *hclsyntax.LiteralValueExpr:
if p.Val.Type() == cty.String {
// Escape special characters in string literals within templates
s := p.Val.AsString()
s = strings.ReplaceAll(s, `\`, `\\`)
s = strings.ReplaceAll(s, `"`, `\"`)
s = strings.ReplaceAll(s, `${`, `$${`)
result.WriteString(s)
} else {
// Non-string literal in template (shouldn't happen normally)
result.WriteString("${")
result.WriteString(literalValueToHCL(p.Val))
result.WriteString("}")
}
default:
// Interpolation
result.WriteString("${")
result.WriteString(NodeToSource(p))
result.WriteString("}")
}
}
result.WriteString(`"`)
return result.String()
}
// literalValueToHCL converts a cty.Value to its HCL literal representation
func literalValueToHCL(val cty.Value) string {
if val.IsNull() {
return "null"
}
switch val.Type() {
case cty.String:
return strconv.Quote(val.AsString())
case cty.Number:
bf := val.AsBigFloat()
if bf.IsInt() {
i, _ := bf.Int64()
return strconv.FormatInt(i, 10)
}
f, _ := bf.Float64()
return strconv.FormatFloat(f, 'f', -1, 64)
case cty.Bool:
if val.True() {
return "true"
}
return "false"
default:
return "<unknown>"
}
}
// operationToHCL converts an operation to its string representation
func operationToHCL(op *hclsyntax.Operation) string {
switch op {
case hclsyntax.OpAdd:
return "+"
case hclsyntax.OpSubtract:
return "-"
case hclsyntax.OpMultiply:
return "*"
case hclsyntax.OpDivide:
return "/"
case hclsyntax.OpModulo:
return "%"
case hclsyntax.OpEqual:
return "=="
case hclsyntax.OpNotEqual:
return "!="
case hclsyntax.OpLessThan:
return "<"
case hclsyntax.OpLessThanOrEqual:
return "<="
case hclsyntax.OpGreaterThan:
return ">"
case hclsyntax.OpGreaterThanOrEqual:
return ">="
case hclsyntax.OpLogicalAnd:
return "&&"
case hclsyntax.OpLogicalOr:
return "||"
case hclsyntax.OpLogicalNot:
return "!"
case hclsyntax.OpNegate:
return "-"
default:
panic(fmt.Errorf("unable to handle operation of type %T", op))
}
}
var reIdent = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_-]*$`)
func IsIdentifier(s string) bool {
return reIdent.MatchString(s)
}
func IsQuotedIdentifier(s string) bool {
return len(s) >= 2 && strings.HasPrefix(s, `"`) && strings.HasSuffix(s, `"`) && IsIdentifier(s[1:len(s)-1])
}
func unwrapIdent(s string) string {
if IsQuotedIdentifier(s) {
return s[1 : len(s)-1]
}
return s
}
// Package handlers provides the core handler implementation that is responsible for managing internal features,
// and dispatching requests to these.
package handlers
import (
"context"
"fmt"
"github.com/creachadair/jrpc2"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langserver/session"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/utils/logging"
)
func NewSession(srvCtx context.Context, serverVersion string) session.Session {
sessCtx, stopSession := context.WithCancel(srvCtx)
return &service{
version: serverVersion,
logger: logging.LoggerFor(logging.ModuleHandlers),
sessCtx: sessCtx,
stopSession: stopSession,
}
}
// Assigner builds out the jrpc2.Map according to the LSP protocol
// and passes related dependencies to handlers via context
func (svc *service) Assigner() (jrpc2.Assigner, error) {
svc.logger.Println("Preparing new session ...")
s := session.NewLifecycle(svc.stopSession)
err := s.Prepare()
if err != nil {
return nil, fmt.Errorf("unable to prepare session: %w", err)
}
return svc.getDispatchTable(s)
}
func (svc *service) Finish(_ jrpc2.Assigner, status jrpc2.ServerStatus) {
if status.Closed || status.Err != nil {
svc.logger.Printf("session stopped unexpectedly (err: %v)", status.Err)
}
svc.stopSession()
}
package handlers
import (
"context"
"fmt"
"time"
"github.com/creachadair/jrpc2"
rpch "github.com/creachadair/jrpc2/handler"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langserver/session"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/utils/logging"
"github.com/pkg/errors"
)
const requestCancelled jrpc2.Code = -32800
func (svc *service) getDispatchTable(s *session.Lifecycle) (rpch.Map, error) {
dispatchTo := func(to any) func(_ context.Context, _ *jrpc2.Request) (any, error) {
return func(ctx context.Context, req *jrpc2.Request) (any, error) {
err := s.CheckInitializationIsConfirmed()
if err != nil {
return nil, err
}
return handle(ctx, req, to)
}
}
m := map[string]rpch.Func{
"initialize": func(ctx context.Context, req *jrpc2.Request) (interface{}, error) {
err := s.Initialize(req)
if err != nil {
return nil, err
}
return handle(ctx, req, svc.initialize)
},
"initialized": func(ctx context.Context, req *jrpc2.Request) (interface{}, error) {
err := s.ConfirmInitialization(req)
if err != nil {
return nil, err
}
return handle(ctx, req, svc.initialized)
},
"shutdown": func(ctx context.Context, req *jrpc2.Request) (interface{}, error) {
err := s.Shutdown(req)
if err != nil {
return nil, err
}
return handle(ctx, req, shutdown)
},
"exit": func(ctx context.Context, req *jrpc2.Request) (interface{}, error) {
err := s.Exit()
if err != nil {
return nil, err
}
svc.stopSession()
return nil, nil
},
"textDocument/didChange": dispatchTo(svc.textDocumentDidChange),
"textDocument/didOpen": dispatchTo(svc.textDocumentDidOpen),
"textDocument/didClose": dispatchTo(svc.textDocumentDidClose),
"textDocument/documentSymbol": dispatchTo(svc.textDocumentSymbol),
"textDocument/documentLink": dispatchTo(svc.textDocumentLink),
"textDocument/declaration": dispatchTo(svc.textDocumentGoToDeclaration),
"textDocument/definition": dispatchTo(svc.textDocumentGoToDefinition),
"textDocument/references": dispatchTo(svc.textDocumentReferences),
"textDocument/completion": dispatchTo(svc.textDocumentCompletion),
"textDocument/hover": dispatchTo(svc.textDocumentHover),
"textDocument/codeLens": dispatchTo(svc.textDocumentCodeLens),
"textDocument/formatting": dispatchTo(svc.textDocumentFormatting),
"textDocument/signatureHelp": dispatchTo(svc.textDocumentSignatureHelp),
"textDocument/semanticTokens/full": dispatchTo(svc.textDocumentSemanticTokensFull),
"textDocument/foldingRange": dispatchTo(svc.textDocumentFoldingRange),
"textDocument/didSave": dispatchTo(svc.textDocumentDidSave),
"workspace/didChangeWorkspaceFolders": dispatchTo(svc.workspaceDidChangeWorkspaceFolders),
"workspace/didChangeWatchedFiles": dispatchTo(svc.workspaceDidChangeWatchedFiles),
"workspace/symbol": dispatchTo(svc.workspaceSymbol),
"$/cancelRequest": dispatchTo(cancelRequest),
}
return convertMap(m), nil
}
// convertMap is a helper function allowing us to omit the jrpc2.Func
// signature from the method definitions
func convertMap(m map[string]rpch.Func) rpch.Map {
hm := make(rpch.Map, len(m))
for method, fun := range m {
hm[method] = rpch.New(fun)
}
return hm
}
// handle calls a jrpc2.Func compatible function
func handle(ctx context.Context, req *jrpc2.Request, fn interface{}) (interface{}, error) {
if logging.PerfLogger != nil {
start := time.Now()
defer func() {
logging.PerfLogger.Printf("req: %s::%s [%s]", req.Method(), req.ID(), time.Since(start))
}()
}
result, err := rpch.New(fn)(ctx, req)
if ctx.Err() != nil && errors.Is(ctx.Err(), context.Canceled) {
err = fmt.Errorf("%w: %s", requestCancelled.Err(), err)
}
return result, err
}
package handlers
import (
"context"
"sync"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/eventbus"
lsp "github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langserver/protocol"
)
const crdSetupURL = "https://github.com/crossplane-contrib/function-hcl/function-hcl-ls/blob/main/README.md"
func (svc *service) startCRDNotificationHandler(ctx context.Context) {
events := svc.eventBus.SubscribeToNoCRDSourcesEvents("handlers.crd_notification")
var notifiedDirs sync.Map
go func() {
for {
select {
case event := <-events:
svc.handleNoCRDSources(ctx, event, ¬ifiedDirs)
case <-ctx.Done():
return
}
}
}()
}
func (svc *service) handleNoCRDSources(ctx context.Context, event eventbus.NoCRDSourcesEvent, notifiedDirs *sync.Map) {
// Only notify once per directory per session
if _, alreadyNotified := notifiedDirs.LoadOrStore(event.Dir, true); alreadyNotified {
return
}
params := &lsp.ShowMessageRequestParams{
Type: lsp.Info,
Message: "No CRD sources configured for this workspace. Configure CRDs for full completion and validation support.",
Actions: []lsp.MessageActionItem{
{Title: "Learn More"},
},
}
resp, err := svc.server.Callback(ctx, "window/showMessageRequest", params)
if err != nil {
svc.logger.Printf("failed to send CRD notification request: %s", err)
return
}
// check if user clicked the action button
var action *lsp.MessageActionItem
if err := resp.UnmarshalResult(&action); err != nil {
svc.logger.Printf("failed to unmarshal showMessageRequest response: %s", err)
return
}
// user dismissed the notification or clicked outside
if action == nil {
return
}
// user clicked "Learn More" - open the URL
if action.Title == "Learn More" {
resp, err := svc.server.Callback(ctx, "window/showDocument", &lsp.ShowDocumentParams{
URI: crdSetupURL,
External: true,
})
if err != nil {
svc.logger.Printf("failed to open CRD setup URL: %s", err)
return
}
var result lsp.ShowDocumentResult
if err := resp.UnmarshalResult(&result); err != nil {
svc.logger.Printf("failed to unmarshal showDocument response: %s", err)
return
}
if !result.Success {
svc.logger.Printf("showDocument reported failure for URL: %s", crdSetupURL)
}
}
}
package handlers
import (
"context"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/eventbus"
ilsp "github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langserver/lsp"
lsp "github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langserver/protocol"
)
func (svc *service) startDiagnosticsPublisher(ctx context.Context) {
diagEvents := svc.eventBus.SubscribeToDiagnosticsEvents("handlers.diagnostics")
go func() {
for {
select {
case event := <-diagEvents:
svc.publishDiagnostics(ctx, event)
case <-ctx.Done():
return
}
}
}()
}
func (svc *service) publishDiagnostics(ctx context.Context, event eventbus.DiagnosticsEvent) {
uri := lsp.DocumentURI(event.Doc.FullURI())
diags := ilsp.HCLDiagsToLSP(event.Diags, "function-hcl")
err := svc.server.Notify(ctx, "textDocument/publishDiagnostics", lsp.PublishDiagnosticsParams{
URI: uri,
Diagnostics: diags,
})
if err != nil {
svc.logger.Printf("failed to publish diagnostics: %s", err)
}
}
package handlers
import (
"context"
"fmt"
"strings"
"github.com/crossplane-contrib/function-hcl/api"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/document"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/document/diff"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/funchcl/decoder"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/funchcl/decoder/completion"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/funchcl/decoder/folding"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/funchcl/decoder/semtok"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/funchcl/decoder/symbols"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/lang"
ilsp "github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langserver/lsp"
lsp "github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langserver/protocol"
"github.com/hashicorp/hcl/v2"
)
func (svc *service) textDocumentCompletion(ctx context.Context, params lsp.CompletionParams) (lsp.CompletionList, error) {
var list lsp.CompletionList
doc, path, err := svc.standardInit(ctx, params.TextDocument.URI)
if err != nil {
return list, err
}
pos, err := ilsp.HCLPositionFromLspPosition(params.Position, doc)
if err != nil {
return list, err
}
pc, err := svc.features.modules.PathCompletionContext(path, doc.Filename, pos)
if err != nil {
return list, err
}
svc.logger.Printf("Looking for candidates at %q -> %#v", doc.Filename, pos)
c := completion.New(pc)
candidates, err := c.CompletionAt(doc.Filename, pos)
if err != nil {
return list, err
}
if behavior := decoder.GetBehavior(); behavior.IndentMultiLineProposals {
indent := ""
lineIdx := int(params.Position.Line)
if lineIdx < len(doc.Lines) {
for _, b := range doc.Lines[lineIdx].Bytes {
if b == ' ' || b == '\t' {
indent += string(b)
} else {
break
}
}
}
if indent != "" {
for i := range candidates.List {
if strings.Contains(candidates.List[i].TextEdit.Snippet, "\n") {
candidates.List[i].TextEdit.Snippet = strings.ReplaceAll(candidates.List[i].TextEdit.Snippet, "\n", "\n"+indent)
}
if strings.Contains(candidates.List[i].TextEdit.NewText, "\n") {
candidates.List[i].TextEdit.NewText = strings.ReplaceAll(candidates.List[i].TextEdit.NewText, "\n", "\n"+indent)
}
}
}
}
svc.logger.Printf("received candidates: %#v", candidates)
out := ilsp.ToCompletionList(candidates, svc.cc.TextDocument)
return out, err
}
func (svc *service) textDocumentHover(ctx context.Context, params lsp.TextDocumentPositionParams) (*lsp.Hover, error) {
doc, _, err := svc.standardInit(ctx, params.TextDocument.URI)
if err != nil {
return nil, err
}
pos, err := ilsp.HCLPositionFromLspPosition(params.Position, doc)
if err != nil {
return nil, err
}
pc, err := svc.features.modules.PathCompletionContext(lang.Path{
Path: doc.Dir.Path(),
LanguageID: doc.LanguageID,
}, doc.Filename, pos)
if err != nil {
return nil, err
}
svc.logger.Printf("Looking for hover data at %q -> %#v", doc.Filename, pos)
c := completion.New(pc)
hoverData, err := c.HoverAt(doc.Filename, pos)
svc.logger.Printf("received hover data: %#v", hoverData)
if err != nil {
svc.logger.Printf("hover at %q %v failed: %v", doc.Filename, pos, err)
return nil, nil // hide this from the client
}
return ilsp.HoverData(hoverData, svc.cc.TextDocument), nil
}
func (svc *service) textDocumentLink(ctx context.Context, params lsp.DocumentLinkParams) ([]lsp.DocumentLink, error) {
doc, _, err := svc.standardInit(ctx, params.TextDocument.URI)
if err != nil {
return nil, err
}
if doc.LanguageID != ilsp.HCL.String() {
return nil, nil
}
// TODO: implement me
return nil, nil
}
func (svc *service) textDocumentGoToDefinition(ctx context.Context, params lsp.TextDocumentPositionParams) (interface{}, error) {
return svc.goToReferenceTarget(ctx, params)
}
func (svc *service) textDocumentGoToDeclaration(ctx context.Context, params lsp.TextDocumentPositionParams) (interface{}, error) {
return svc.goToReferenceTarget(ctx, params)
}
func (svc *service) goToReferenceTarget(ctx context.Context, params lsp.TextDocumentPositionParams) ([]lsp.LocationLink, error) {
doc, path, err := svc.standardInit(ctx, params.TextDocument.URI)
if err != nil {
return nil, err
}
pos, err := ilsp.HCLPositionFromLspPosition(params.Position, doc)
if err != nil {
return nil, err
}
refMap, err := svc.features.modules.ReferenceMap(path)
if err != nil {
return nil, err
}
svc.logger.Printf("Looking for definition from %q -> %#v", doc.Filename, pos)
def := refMap.FindDefinitionFromReference(doc.Filename, pos)
if def == nil {
return ilsp.ToLocationLinks(path, []hcl.Range{}), nil
}
return ilsp.ToLocationLinks(path, []hcl.Range{*def}), nil
}
func (svc *service) textDocumentReferences(ctx context.Context, params lsp.ReferenceParams) ([]lsp.Location, error) {
var list []lsp.Location
doc, path, err := svc.standardInit(ctx, params.TextDocument.URI)
if err != nil {
return list, err
}
pos, err := ilsp.HCLPositionFromLspPosition(params.Position, doc)
if err != nil {
return list, err
}
refMap, err := svc.features.modules.ReferenceMap(path)
if err != nil {
return nil, err
}
svc.logger.Printf("Looking for references from %q -> %#v", doc.Filename, pos)
refs := refMap.FindReferencesFromDefinition(doc.Filename, pos)
return ilsp.ToLocations(path, refs), nil
}
func (svc *service) textDocumentSignatureHelp(ctx context.Context, params lsp.SignatureHelpParams) (*lsp.SignatureHelp, error) {
doc, path, err := svc.standardInit(ctx, params.TextDocument.URI)
if err != nil {
return nil, err
}
pos, err := ilsp.HCLPositionFromLspPosition(params.Position, doc)
if err != nil {
return nil, err
}
pc, err := svc.features.modules.PathCompletionContext(path, doc.Filename, pos)
if err != nil {
return nil, err
}
c := completion.New(pc)
sig, err := c.SignatureAtPos(doc.Filename, pos)
if err != nil {
return nil, err
}
return ilsp.ToSignatureHelp(sig), nil
}
func (svc *service) textDocumentSymbol(ctx context.Context, params lsp.DocumentSymbolParams) ([]lsp.DocumentSymbol, error) {
doc, path, err := svc.standardInit(ctx, params.TextDocument.URI)
if err != nil {
return nil, err
}
pc, err := svc.features.modules.PathContext(path)
if err != nil {
return nil, err
}
c := symbols.NewCollector(path)
syms, err := c.FileSymbols(pc, doc.Filename)
if err != nil {
return nil, err
}
return ilsp.DocumentSymbols(syms, svc.cc.TextDocument.DocumentSymbol), nil
}
func (svc *service) textDocumentSemanticTokensFull(ctx context.Context, params lsp.SemanticTokensParams) (ret lsp.SemanticTokens, _ error) {
// TODO: check client capabilities, full request etc.
doc, path, err := svc.standardInit(ctx, params.TextDocument.URI)
if err != nil {
return ret, err
}
pc, err := svc.features.modules.PathContext(path)
if err != nil {
return ret, err
}
tokens, err := semtok.TokensFor(pc, doc.Filename)
if err != nil {
return ret, err
}
enc := ilsp.NewTokenEncoder(tokens, doc.Lines)
ret.Data = enc.Encode()
return ret, nil
}
func (svc *service) textDocumentCodeLens(ctx context.Context, params lsp.CodeLensParams) ([]lsp.CodeLens, error) {
var list []lsp.CodeLens
_, _, err := svc.standardInit(ctx, params.TextDocument.URI)
if err != nil {
return list, err
}
/*
not yet implemented
*/
return list, nil
}
func (svc *service) textDocumentFormatting(ctx context.Context, params lsp.DocumentFormattingParams) (_ []lsp.TextEdit, finalErr error) {
dh := ilsp.HandleFromDocumentURI(params.TextDocument.URI)
doc, err := svc.docStore.Get(dh)
if err != nil {
return nil, err
}
return svc.formatDocument(ctx, doc.Text, dh)
}
func (svc *service) formatDocument(ctx context.Context, original []byte, dh document.Handle) ([]lsp.TextEdit, error) {
formatted := []byte(api.FormatHCL(string(original)))
if len(formatted) == 0 {
return nil, fmt.Errorf("format failed")
}
changes := diff.Diff(dh, original, formatted)
return ilsp.TextEditsFromDocumentChanges(changes), nil
}
func (svc *service) textDocumentFoldingRange(ctx context.Context, params lsp.FoldingRangeParams) ([]lsp.FoldingRange, error) {
doc, path, err := svc.standardInit(ctx, params.TextDocument.URI)
if err != nil {
return nil, err
}
pc, err := svc.features.modules.PathContext(path)
if err != nil {
return nil, err
}
file, ok := pc.HCLFileByName(doc.Filename)
if !ok {
return nil, fmt.Errorf("file %s not found", doc.Filename)
}
behavior := pc.Behavior()
ranges := folding.Collect(file, behavior)
if behavior.InnerBraceRangesForFolding {
// Adjust end positions: end at the last character of the line before the closing brace.
// This satisfies lsp4ij's charAt requirements.
for i := range ranges {
if ranges[i].EndLine > 1 {
prevLineIdx := ranges[i].EndLine - 2 // convert to 0-based index for previous line
if prevLineIdx >= 0 && prevLineIdx < len(doc.Lines) {
ranges[i].EndLine--
ranges[i].EndColumn = len(doc.Lines[prevLineIdx].Bytes)
}
}
}
}
return ilsp.FoldingRanges(ranges), nil
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package handlers
import (
"context"
"fmt"
"github.com/creachadair/jrpc2"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/document"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/eventbus"
ilsp "github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langserver/lsp"
lsp "github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langserver/protocol"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/utils/uri"
)
func (svc *service) textDocumentDidOpen(ctx context.Context, params lsp.DidOpenTextDocumentParams) error {
docURI := string(params.TextDocument.URI)
// URIs are always checked during initialize request, but
// we still allow single-file mode, therefore invalid URIs
// can still land here, so we check for those.
if !uri.IsURIValid(docURI) {
_ = jrpc2.ServerFromContext(ctx).Notify(ctx, "window/showMessage", &lsp.ShowMessageParams{
Type: lsp.Warning,
Message: fmt.Sprintf("Ignoring workspace folder (unsupport or invalid URI) %s."+
" This is most likely bug, please report it.", docURI),
})
return fmt.Errorf("invalid URI: %s", docURI)
}
dh := document.HandleFromURI(docURI)
err := svc.docStore.Open(dh, params.TextDocument.LanguageID,
int(params.TextDocument.Version), []byte(params.TextDocument.Text))
if err != nil {
return err
}
svc.eventBus.PublishOpenEvent(eventbus.OpenEvent{
Doc: dh,
LanguageID: params.TextDocument.LanguageID,
})
return nil
}
func (svc *service) textDocumentDidChange(ctx context.Context, params lsp.DidChangeTextDocumentParams) error {
p := lsp.DidChangeTextDocumentParams{
TextDocument: lsp.VersionedTextDocumentIdentifier{
TextDocumentIdentifier: lsp.TextDocumentIdentifier{
URI: params.TextDocument.URI,
},
Version: params.TextDocument.Version,
},
ContentChanges: params.ContentChanges,
}
dh := ilsp.HandleFromDocumentURI(p.TextDocument.URI)
doc, err := svc.docStore.Get(dh)
if err != nil {
svc.logger.Println("GetDocument error:", err)
return err
}
newVersion := int(p.TextDocument.Version)
// Versions don't have to be consecutive, but they must be increasing
if newVersion <= doc.Version {
svc.logger.Printf("Old document version (%d) received, current version is %d. "+
"Ignoring this update for %s. This is likely a client bug, please report it.",
newVersion, doc.Version, p.TextDocument.URI)
return nil
}
changes := ilsp.DocumentChanges(params.ContentChanges)
newText, err := document.ApplyChanges(doc.Text, changes)
if err != nil {
svc.logger.Println("ApplyChanges error:", err)
return err
}
err = svc.docStore.Update(dh, newText, newVersion)
if err != nil {
svc.logger.Println("updateDocument error:", err)
return err
}
svc.eventBus.PublishEditEvent(eventbus.EditEvent{
Doc: dh,
LanguageID: doc.LanguageID,
})
return nil
}
func (svc *service) textDocumentDidSave(ctx context.Context, params lsp.DidSaveTextDocumentParams) error {
// TODO: maybe implement validate on save
return nil
}
func (svc *service) textDocumentDidClose(ctx context.Context, params lsp.DidCloseTextDocumentParams) error {
dh := ilsp.HandleFromDocumentURI(params.TextDocument.URI)
return svc.docStore.Close(dh)
}
package handlers
import (
"context"
"encoding/json"
"fmt"
"log"
"github.com/creachadair/jrpc2"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/funchcl/decoder"
ilsp "github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langserver/lsp"
lsp "github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langserver/protocol"
"github.com/hashicorp/go-uuid"
)
func (svc *service) initialize(ctx context.Context, params lsp.InitializeParams) (lsp.InitializeResult, error) {
b, _ := json.MarshalIndent(params, "", " ")
log.Println("Initializing:\n", string(b))
decoder.SetBehavior(behaviorFromClientInfo(params.ClientInfo))
serverCaps := initializeResult(svc.version)
svc.server = jrpc2.ServerFromContext(ctx)
svc.cc = ¶ms.Capabilities
err := svc.configureSessionDependencies()
if err != nil {
return serverCaps, err
}
return serverCaps, nil
}
func initializeResult(serverVersion string) lsp.InitializeResult {
serverCaps := lsp.InitializeResult{
Capabilities: lsp.ServerCapabilities{
TextDocumentSync: lsp.TextDocumentSyncOptions{
OpenClose: true,
Change: lsp.Incremental,
},
CompletionProvider: lsp.CompletionOptions{
ResolveProvider: false,
TriggerCharacters: []string{".", "["},
},
//CodeActionProvider: lsp.CodeActionOptions{
// CodeActionKinds: ilsp.SupportedCodeActions.AsSlice(),
// ResolveProvider: false,
//},
DeclarationProvider: true,
DefinitionProvider: true,
// define this explicitly to be an empty command list because
// intellij does not like nil values for this attribute.
ExecuteCommandProvider: lsp.ExecuteCommandOptions{
Commands: []string{},
WorkDoneProgressOptions: lsp.WorkDoneProgressOptions{WorkDoneProgress: false},
},
// CodeLensProvider: &lsp.CodeLensOptions{},
ReferencesProvider: true,
HoverProvider: true,
DocumentFormattingProvider: true,
DocumentSymbolProvider: true,
WorkspaceSymbolProvider: true,
Workspace: lsp.Workspace6Gn{
WorkspaceFolders: lsp.WorkspaceFolders5Gn{
Supported: true,
ChangeNotifications: "workspace/didChangeWorkspaceFolders",
},
},
SemanticTokensProvider: lsp.SemanticTokensOptions{
Full: true,
Legend: lsp.SemanticTokensLegend{
TokenTypes: ilsp.TokenTypesLegend().AsStrings(),
TokenModifiers: ilsp.TokenModifiersLegend().AsStrings(),
},
},
SignatureHelpProvider: lsp.SignatureHelpOptions{
TriggerCharacters: []string{"(", ","},
},
FoldingRangeProvider: true,
},
}
serverCaps.ServerInfo.Name = "function-hcl-ls"
serverCaps.ServerInfo.Version = serverVersion
return serverCaps
}
func (svc *service) initialized(ctx context.Context, params lsp.InitializedParams) error {
return svc.setupWatchedFiles(ctx, svc.cc.Workspace.DidChangeWatchedFiles)
}
func (svc *service) setupWatchedFiles(ctx context.Context, caps lsp.DidChangeWatchedFilesClientCapabilities) error {
if !caps.DynamicRegistration {
svc.logger.Printf("Client doesn't support dynamic watched files registration, " +
"provider and module changes may not be reflected at runtime")
return nil
}
id, err := uuid.GenerateUUID()
if err != nil {
return err
}
srv := jrpc2.ServerFromContext(ctx)
_, err = srv.Callback(ctx, "client/registerCapability", lsp.RegistrationParams{
Registrations: []lsp.Registration{
{
ID: id,
Method: "workspace/didChangeWatchedFiles",
RegisterOptions: lsp.DidChangeWatchedFilesRegistrationOptions{
Watchers: []lsp.FileSystemWatcher{
{
GlobPattern: "**/*",
Kind: lsp.WatchCreate | lsp.WatchDelete | lsp.WatchChange,
},
},
},
},
},
})
if err != nil {
svc.logger.Printf("failed to register watched files: %s", err)
} else {
svc.logger.Printf("registered watched files: %s", id)
}
return nil
}
func shutdown(ctx context.Context, _ interface{}) error {
return nil
}
func cancelRequest(ctx context.Context, params lsp.CancelParams) error {
id, err := decodeRequestID(params.ID)
if err != nil {
return err
}
jrpc2.ServerFromContext(ctx).CancelRequest(id)
return nil
}
func behaviorFromClientInfo(clientInfo lsp.Msg_XInitializeParams_clientInfo) decoder.LangServerBehavior {
log.Printf("Client info: name=%q version=%q", clientInfo.Name, clientInfo.Version)
var ret decoder.LangServerBehavior
if clientInfo.Name == "function-hcl-intellij" {
ret.MaxCompletionItems = 1000
ret.InnerBraceRangesForFolding = true
ret.IndentMultiLineProposals = true
}
return ret
}
func decodeRequestID(v interface{}) (string, error) {
if val, ok := v.(string); ok {
return val, nil
}
if val, ok := v.(float64); ok {
return fmt.Sprintf("%d", int64(val)), nil
}
return "", fmt.Errorf("unable to decode request ID: %#v", v)
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package handlers
import (
"context"
"log"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/document"
docstore "github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/document/store"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/eventbus"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/features/crds"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/features/modules"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/filesystem"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/lang"
ilsp "github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langserver/lsp"
lsp "github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langserver/protocol"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langserver/session"
)
type features struct {
modules *modules.Modules
crds *crds.CRDs
}
type service struct {
version string
cc *lsp.ClientCapabilities
logger *log.Logger
sessCtx context.Context
stopSession context.CancelFunc
docStore *docstore.Store
fs *filesystem.Filesystem
server session.Server
eventBus *eventbus.EventBus
features *features
}
func (svc *service) configureSessionDependencies() error {
if svc.docStore == nil {
svc.docStore = docstore.New()
}
if svc.fs == nil {
svc.fs = filesystem.New(svc.docStore)
}
if svc.eventBus == nil {
svc.eventBus = eventbus.New()
}
if svc.features == nil {
c := crds.New(crds.Config{
EventBus: svc.eventBus,
})
c.Start(svc.sessCtx)
m, err := modules.New(modules.Config{
EventBus: svc.eventBus,
DocStore: svc.docStore,
FS: svc.fs,
Provider: func(path string) modules.DynamicSchemas {
return c.DynamicSchemas(path)
},
})
if err != nil {
return err
}
m.Start(svc.sessCtx)
svc.features = &features{
modules: m,
crds: c,
}
}
svc.startDiagnosticsPublisher(svc.sessCtx)
svc.startCRDNotificationHandler(svc.sessCtx)
return nil
}
func (svc *service) standardInit(_ context.Context, uri lsp.DocumentURI) (_ *document.Document, p lang.Path, _ error) {
dh := ilsp.HandleFromDocumentURI(uri)
doc, err := svc.docStore.Get(dh)
if err != nil {
return nil, p, err
}
svc.features.modules.WaitUntilProcessed(dh.Dir.Path())
p = lang.Path{
Path: doc.Dir.Path(),
LanguageID: doc.LanguageID,
}
return doc, p, nil
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package handlers
import (
"context"
"os"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/eventbus"
lsp "github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langserver/protocol"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/utils/uri"
)
func (svc *service) workspaceDidChangeWatchedFiles(_ context.Context, params lsp.DidChangeWatchedFilesParams) error {
for _, change := range params.Changes {
svc.logger.Printf("received change event for %q: %s", change.Type, change.URI)
rawURI := string(change.URI)
rawPath, err := uri.PathFromURI(rawURI)
if err != nil {
svc.logger.Printf("error parsing %q: %s", rawURI, err)
continue
}
isDir := false
switch change.Type {
case lsp.Changed, lsp.Created:
fi, err := os.Stat(rawPath)
if err != nil {
svc.logger.Printf("error checking existence (%q changed): %s", rawPath, err)
continue
}
isDir = fi.IsDir()
}
svc.eventBus.PublishChangeWatchEvent(eventbus.ChangeWatchEvent{
RawPath: rawPath,
IsDir: isDir,
ChangeType: change.Type,
})
}
return nil
}
func (svc *service) workspaceDidChangeWorkspaceFolders(ctx context.Context, params lsp.DidChangeWorkspaceFoldersParams) error {
return nil
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package handlers
import (
"context"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/funchcl/decoder/symbols"
ilsp "github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langserver/lsp"
lsp "github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langserver/protocol"
)
func (svc *service) workspaceSymbol(ctx context.Context, params lsp.WorkspaceSymbolParams) ([]lsp.SymbolInformation, error) {
syms, err := symbols.WorkspaceSymbols(svc.features.modules, params.Query)
if err != nil {
return nil, err
}
return ilsp.WorkspaceSymbols(syms, svc.cc.Workspace.Symbol), nil
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
// Package langserver implements the language server endpoints.
package langserver
import (
"context"
"fmt"
"io"
"log"
"net"
"os"
"runtime"
"github.com/creachadair/jrpc2"
"github.com/creachadair/jrpc2/channel"
"github.com/creachadair/jrpc2/server"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langserver/handlers"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langserver/session"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/utils/logging"
)
type Options struct {
ServerVersion string
Concurrency int
Factory session.Factory
}
type LangServer struct {
version string
srvCtx context.Context
logger *log.Logger
srvOptions *jrpc2.ServerOptions
newSession session.Factory
}
func (o *Options) setDefaults() {
if o.ServerVersion == "" {
o.ServerVersion = "1.0"
}
if o.Factory == nil {
o.Factory = handlers.NewSession
}
if o.Concurrency <= 0 {
o.Concurrency = DefaultConcurrency()
}
}
func New(srvCtx context.Context, opts Options) *LangServer {
opts.setDefaults()
logger := logging.LoggerFor(logging.ModuleLangServer)
rpcOpts := &jrpc2.ServerOptions{
AllowPush: true,
Concurrency: opts.Concurrency,
Logger: jrpc2.StdLogger(logger),
RPCLog: &rpcLogger{logger},
}
return &LangServer{
version: opts.ServerVersion,
srvCtx: srvCtx,
logger: logger,
srvOptions: rpcOpts,
newSession: opts.Factory,
}
}
func DefaultConcurrency() int {
cpu := runtime.NumCPU()
// Cap concurrency on powerful machines
// to leave some capacity for module ops
// and other application
if cpu >= 4 {
return cpu / 2
}
return cpu
}
func (ls *LangServer) newService() server.Service {
return ls.newSession(ls.srvCtx, ls.version)
}
func (ls *LangServer) startServer(reader io.Reader, writer io.WriteCloser) (*singleServer, error) {
srv, err := getServer(ls.newService(), ls.srvOptions)
if err != nil {
return nil, err
}
srv.Start(channel.LSP(reader, writer))
return srv, nil
}
func (ls *LangServer) StartAndWait(reader io.Reader, writer io.WriteCloser) error {
srv, err := ls.startServer(reader, writer)
if err != nil {
return err
}
ls.logger.Printf("Starting server (pid %d; concurrency: %d) ...",
os.Getpid(), ls.srvOptions.Concurrency)
// Wrap waiter with a context so that we can cancel it here
// after the service is cancelled (and srv.Wait returns)
ctx, cancelFunc := context.WithCancel(ls.srvCtx)
go func() {
srv.Wait()
cancelFunc()
}()
<-ctx.Done()
ls.logger.Printf("Stopping server (pid %d) ...", os.Getpid())
srv.Stop()
ls.logger.Printf("Server (pid %d) stopped.", os.Getpid())
return nil
}
func (ls *LangServer) StartTCP(address string) error {
ls.logger.Printf("Starting TCP server (pid %d; concurrency: %d) at %q ...",
os.Getpid(), ls.srvOptions.Concurrency, address)
lst, err := net.Listen("tcp", address)
if err != nil {
return fmt.Errorf("TCP Server failed to start: %s", err)
}
ls.logger.Printf("TCP server running at %q", lst.Addr())
accepter := server.NetAccepter(lst, channel.LSP)
go func() {
ls.logger.Println("Starting loop server ...")
err = server.Loop(context.TODO(), accepter, ls.newService, &server.LoopOptions{
ServerOptions: ls.srvOptions,
})
if err != nil {
ls.logger.Printf("Loop server failed to start: %s", err)
}
}()
<-ls.srvCtx.Done()
ls.logger.Printf("Stopping TCP server (pid %d) ...", os.Getpid())
err = lst.Close()
if err != nil {
ls.logger.Printf("TCP server (pid %d) failed to stop: %s", os.Getpid(), err)
return err
}
ls.logger.Printf("TCP server (pid %d) stopped.", os.Getpid())
return nil
}
// singleServer is a wrapper around jrpc2.NewServer providing support
// for server.Service (Assigner/Finish interface)
type singleServer struct {
srv *jrpc2.Server
finishFunc func(jrpc2.ServerStatus)
}
func getServer(svc server.Service, opts *jrpc2.ServerOptions) (*singleServer, error) {
assigner, err := svc.Assigner()
if err != nil {
return nil, err
}
return &singleServer{
srv: jrpc2.NewServer(assigner, opts),
finishFunc: func(status jrpc2.ServerStatus) {
svc.Finish(assigner, status)
},
}, nil
}
func (ss *singleServer) Start(ch channel.Channel) {
ss.srv = ss.srv.Start(ch)
}
func (ss *singleServer) StartAndWait(ch channel.Channel) {
ss.Start(ch)
ss.Wait()
}
func (ss *singleServer) Wait() {
status := ss.srv.WaitStatus()
ss.finishFunc(status)
}
func (ss *singleServer) Stop() {
ss.srv.Stop()
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package langserver
import (
"bytes"
"context"
"encoding/json"
"errors"
"io"
"log"
"os"
"testing"
"time"
"github.com/creachadair/jrpc2"
"github.com/creachadair/jrpc2/channel"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/utils/logging"
"github.com/google/go-cmp/cmp"
)
type T interface {
Fatal(args ...interface{})
Fatalf(format string, args ...interface{})
}
type Mock struct {
srv *LangServer
logger *log.Logger
// rpcSrv is set when server starts and allows Stop() to stop it after testing is finished
rpcSrv *singleServer
srvStopFunc context.CancelFunc
stopFuncCalled bool
srvStdin io.Reader
srvStdout io.WriteCloser
client *jrpc2.Client
clientStdin io.Reader
clientStdout io.WriteCloser
}
func NewMock(t T, opts Options) *Mock {
opts.setDefaults()
stdinReader, stdinWriter := io.Pipe()
stdoutReader, stdoutWriter := io.Pipe()
logger := logging.NopLogger()
if testing.Verbose() {
logger = testLogger(os.Stdout, "")
}
srvCtx, stopFunc := context.WithCancel(context.Background())
lsm := &Mock{
logger: logger,
srvStopFunc: stopFunc,
srvStdin: stdinReader,
srvStdout: stdoutWriter,
clientStdin: stdoutReader,
clientStdout: stdinWriter,
}
lsm.srv = New(srvCtx, opts)
return lsm
}
func (lsm *Mock) Stop() {
lsm.logger.Println("Stopping mock server ...")
lsm.rpcSrv.Stop()
lsm.stopFuncCalled = true
}
func (lsm *Mock) StopFuncCalled() bool {
return lsm.stopFuncCalled
}
// Start is more or less duplicate of LangServer.StartAndWait
// except that this one doesn't wait
//
// TODO: Explore whether we could leverage jrpc2's server.Local
func (lsm *Mock) Start(t T) context.CancelFunc {
lsm.logger.Println("Starting mock server ...")
srv, err := lsm.srv.startServer(lsm.srvStdin, lsm.srvStdout)
if err != nil {
t.Fatal(err)
}
lsm.rpcSrv = srv
go func() {
lsm.rpcSrv.Wait()
}()
clientCh := channel.LSP(lsm.clientStdin, lsm.clientStdout)
opts := &jrpc2.ClientOptions{}
if testing.Verbose() {
opts.Logger = jrpc2.StdLogger(testLogger(os.Stdout, "[CLIENT] "))
}
lsm.client = jrpc2.NewClient(clientCh, opts)
return lsm.Stop
}
func (lsm *Mock) CloseClientStdout(t T) {
err := lsm.clientStdout.Close()
if err != nil {
t.Fatal(err)
}
}
type CallRequest struct {
Method string
ReqParams string
}
func (lsm *Mock) Call(t T, cr *CallRequest) *rawResponse {
rsp, err := lsm.client.Call(context.Background(), cr.Method, json.RawMessage(cr.ReqParams))
if err != nil {
t.Fatal(err)
}
b, err := rsp.MarshalJSON()
if err != nil {
t.Fatal(err)
}
r := &rawResponse{}
err = r.UnmarshalJSON(b)
if err != nil {
t.Fatal(err)
}
return r
}
func (lsm *Mock) CallAndExpectResponse(t *testing.T, cr *CallRequest, expectRaw string) {
rsp := lsm.Call(t, cr)
// Compacting is necessary because we retain params as json.RawMessage
// in rawResponse, which holds formatted bytes that may not match
// due to whitespaces
compactedRaw := bytes.NewBuffer([]byte{})
err := json.Compact(compactedRaw, []byte(expectRaw))
if err != nil {
t.Fatal(err)
}
expectedRsp := &rawResponse{}
err = expectedRsp.UnmarshalJSON(compactedRaw.Bytes())
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(expectedRsp, rsp); diff != "" {
t.Fatalf("%q response doesn't match.\n%s",
cr.Method, diff)
}
}
func (lsm *Mock) CallAndExpectError(t *testing.T, cr *CallRequest, expectErr error) {
_, err := lsm.client.Call(context.Background(), cr.Method, json.RawMessage(cr.ReqParams))
if err == nil {
t.Fatalf("expected error: %s", expectErr.Error())
}
if expErr, ok := expectErr.(*jrpc2.Error); ok {
givenErr, ok := err.(*jrpc2.Error)
if !ok {
t.Fatalf("%q error doesn't match.\nexpected: %#v\ngiven: %#v\n",
cr.Method, expectErr, err)
}
if expErr.Code != givenErr.Code || expErr.Message != givenErr.Message {
t.Fatalf("%q error doesn't match.\nexpected: %#v\ngiven: %#v\n",
cr.Method, expectErr, err)
}
return
}
if !errors.Is(expectErr, err) {
t.Fatalf("%q error doesn't match.\nexpected: %#v\ngiven: %#v\n",
cr.Method, expectErr, err)
}
}
func (lsm *Mock) Notify(t *testing.T, cr *CallRequest) {
err := lsm.client.Notify(context.Background(), cr.Method, json.RawMessage(cr.ReqParams))
// This is to account for the fact that
// notifications are asynchronous in nature per LSP spec.
//
// We assume the server under test has no other notifications
// to process and the method is quick to execute.
//
// TODO: We may need to re-evaluate this hack later and check
// if the server could be turned into sync mode somehow
time.Sleep(1 * time.Millisecond)
if err != nil {
t.Fatal(err)
}
}
// rawResponse is a copy of jrpc2.jresponse
// to enable accurate comparison of responses
type rawResponse struct {
JSONRPC string `json:"jsonrpc"`
ID json.RawMessage `json:"id,omitempty"`
Error *jrpc2.Error `json:"error,omitempty"`
Result json.RawMessage `json:"result,omitempty"`
Method string `json:"method,omitempty"`
Params json.RawMessage `json:"params,omitempty"`
}
func (r *rawResponse) UnmarshalJSON(b []byte) error {
type t rawResponse
var resp t
err := json.Unmarshal(b, &resp)
if err != nil {
return err
}
*r = *(*rawResponse)(&resp)
return nil
}
func testLogger(w io.Writer, prefix string) *log.Logger {
return log.New(w, prefix, log.LstdFlags|log.Lshortfile)
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package lsp
import (
"sort"
lsp "github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langserver/protocol"
)
const (
// SourceFormatAllTerraform is a Terraform specific format code action.
SourceFormatAllTerraform = "source.formatAll.terraform"
)
type CodeActions map[lsp.CodeActionKind]bool
// `source.*`: Source code actions apply to the entire file. They must be explicitly
// requested and will not show in the normal lightbulb menu. Source actions
// can be run on save using editor.codeActionsOnSave and are also shown in
// the source context menu.
// For action definitions, refer to: https://code.visualstudio.com/api/references/vscode-api#CodeActionKind
// `source.fixAll`: Fix all actions automatically fix errors that have a clear fix that do
// not require user input. They should not suppress errors or perform unsafe
// fixes such as generating new types or classes.
// ** We don't support this as terraform fmt only adjusts style**
// lsp.SourceFixAll: true,
// `source.formatAll`: Generic format code action.
// We do not register this for terraform to allow fine grained selection of actions.
// A user should be able to set `source.formatAll` to true, and source.formatAll.terraform to false to allow all
// files to be formatted, but not terraform files (or vice versa).
var SupportedCodeActions = CodeActions{
SourceFormatAllTerraform: true,
}
func (c CodeActions) AsSlice() []lsp.CodeActionKind {
var s []lsp.CodeActionKind
for v := range c {
s = append(s, v)
}
sort.SliceStable(s, func(i, j int) bool {
return string(s[i]) < string(s[j])
})
return s
}
func (c CodeActions) Only(only []lsp.CodeActionKind) CodeActions {
wanted := CodeActions{}
for _, kind := range only {
if v, ok := c[kind]; ok {
wanted[kind] = v
}
}
return wanted
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package lsp
import (
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/lang"
lsp "github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langserver/protocol"
)
func Command(cmd lang.Command) (lsp.Command, error) {
lspCmd := lsp.Command{
Title: cmd.Title,
Command: cmd.ID,
}
for _, arg := range cmd.Arguments {
jsonArg, err := arg.MarshalJSON()
if err != nil {
return lspCmd, err
}
lspCmd.Arguments = append(lspCmd.Arguments, jsonArg)
}
return lspCmd, nil
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package lsp
import (
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/lang"
lsp "github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langserver/protocol"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/utils/mdplain"
)
func ToCompletionList(candidates lang.Candidates, caps lsp.TextDocumentClientCapabilities) lsp.CompletionList {
list := lsp.CompletionList{
Items: make([]lsp.CompletionItem, len(candidates.List)),
IsIncomplete: !candidates.IsComplete,
}
for i, c := range candidates.List {
list.Items[i] = toCompletionItem(c, caps.Completion)
}
return list
}
func toCompletionItem(candidate lang.Candidate, caps lsp.CompletionClientCapabilities) lsp.CompletionItem {
snippetSupport := caps.CompletionItem.SnippetSupport
doc := candidate.Description.Value()
// TODO: Revisit when MarkupContent is allowed as Documentation
// https://github.com/golang/tools/blob/4783bc9b/internal/lsp/protocol/tsprotocol.go#L753
doc = mdplain.Clean(doc)
var kind lsp.CompletionItemKind
switch candidate.Kind {
case lang.AttributeCandidateKind:
kind = lsp.PropertyCompletion
case lang.BlockCandidateKind:
kind = lsp.ClassCompletion
case lang.LabelCandidateKind:
kind = lsp.FieldCompletion
case lang.BoolCandidateKind:
kind = lsp.EnumMemberCompletion
case lang.StringCandidateKind:
kind = lsp.TextCompletion
case lang.NumberCandidateKind:
kind = lsp.ValueCompletion
case lang.KeywordCandidateKind:
kind = lsp.KeywordCompletion
case lang.ListCandidateKind, lang.SetCandidateKind, lang.TupleCandidateKind:
kind = lsp.EnumCompletion
case lang.MapCandidateKind, lang.ObjectCandidateKind:
kind = lsp.StructCompletion
case lang.ReferenceCandidateKind:
kind = lsp.VariableCompletion
}
// TODO: Omit item which uses kind unsupported by the client
var cmd *lsp.Command
if candidate.TriggerSuggest && snippetSupport {
cmd = &lsp.Command{
Command: "editor.action.triggerSuggest",
Title: "Suggest",
}
}
item := lsp.CompletionItem{
Label: candidate.Label,
Kind: kind,
InsertTextFormat: insertTextFormat(snippetSupport),
Detail: candidate.Detail,
Documentation: doc,
TextEdit: textEdit(candidate.TextEdit, snippetSupport),
Command: cmd,
AdditionalTextEdits: TextEdits(candidate.AdditionalTextEdits, snippetSupport),
SortText: candidate.SortText,
}
if candidate.ResolveHook != nil {
item.Data = candidate.ResolveHook
}
if caps.CompletionItem.DeprecatedSupport {
item.Deprecated = candidate.IsDeprecated
}
if tagSliceContains(caps.CompletionItem.TagSupport.ValueSet,
lsp.ComplDeprecated) && candidate.IsDeprecated {
item.Tags = []lsp.CompletionItemTag{
lsp.ComplDeprecated,
}
}
return item
}
func tagSliceContains(supported []lsp.CompletionItemTag, tag lsp.CompletionItemTag) bool {
for _, item := range supported {
if item == tag {
return true
}
}
return false
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package lsp
import (
lsp "github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langserver/protocol"
"github.com/hashicorp/hcl/v2"
)
func HCLSeverityToLSP(severity hcl.DiagnosticSeverity) lsp.DiagnosticSeverity {
var sev lsp.DiagnosticSeverity
switch severity {
case hcl.DiagError:
sev = lsp.SeverityError
case hcl.DiagWarning:
sev = lsp.SeverityWarning
case hcl.DiagInvalid:
panic("invalid diagnostic")
}
return sev
}
func HCLDiagsToLSP(hclDiags hcl.Diagnostics, source string) []lsp.Diagnostic {
diags := []lsp.Diagnostic{}
for _, hclDiag := range hclDiags {
msg := hclDiag.Summary
if hclDiag.Detail != "" {
msg += ": " + hclDiag.Detail
}
var rnge lsp.Range
if hclDiag.Subject != nil {
rnge = HCLRangeToLSP(*hclDiag.Subject)
}
diags = append(diags, lsp.Diagnostic{
Range: rnge,
Severity: HCLSeverityToLSP(hclDiag.Severity),
Source: source,
Message: msg,
})
}
return diags
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package lsp
import (
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/lang"
lsp "github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langserver/protocol"
)
func Links(links []lang.Link, caps *lsp.DocumentLinkClientCapabilities) []lsp.DocumentLink {
docLinks := make([]lsp.DocumentLink, len(links))
for i, link := range links {
tooltip := ""
if caps != nil && caps.TooltipSupport {
tooltip = link.Tooltip
}
docLinks[i] = lsp.DocumentLink{
Range: HCLRangeToLSP(link.Range),
Target: link.URI,
Tooltip: tooltip,
}
}
return docLinks
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package lsp
import (
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/document"
lsp "github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langserver/protocol"
)
type contentChange struct {
text string
rng *document.Range
}
func getContentChange(chEvent lsp.TextDocumentContentChangeEvent) document.Change {
return &contentChange{
text: chEvent.Text,
rng: lspRangeToDocRange(chEvent.Range),
}
}
func DocumentChanges(events []lsp.TextDocumentContentChangeEvent) document.Changes {
changes := make(document.Changes, len(events))
for i, event := range events {
ch := getContentChange(event)
changes[i] = ch
}
return changes
}
func (fc *contentChange) Text() string {
return fc.text
}
func (fc *contentChange) Range() *document.Range {
return fc.rng
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package lsp
import (
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/document"
lsp "github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langserver/protocol"
)
func HandleFromDocumentURI(docUri lsp.DocumentURI) document.Handle {
return document.HandleFromURI(string(docUri))
}
package lsp
import (
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/funchcl/decoder/folding"
lsp "github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langserver/protocol"
)
// FoldingRanges converts internal folding ranges to LSP folding ranges.
// Input ranges use 1-based line/column (HCL convention).
// Output ranges use 0-based line/column (LSP convention).
func FoldingRanges(ranges []folding.Range) []lsp.FoldingRange {
result := make([]lsp.FoldingRange, 0, len(ranges))
for _, r := range ranges {
result = append(result, lsp.FoldingRange{
StartLine: uint32(r.StartLine - 1),
StartCharacter: uint32(r.StartColumn - 1),
EndLine: uint32(r.EndLine - 1),
EndCharacter: uint32(r.EndColumn - 1),
Kind: r.Kind,
})
}
return result
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package lsp
import (
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/lang"
lsp "github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langserver/protocol"
)
func HoverData(data *lang.HoverData, cc lsp.TextDocumentClientCapabilities) *lsp.Hover {
if data == nil {
return nil
}
mdSupported := len(cc.Hover.ContentFormat) > 0 &&
cc.Hover.ContentFormat[0] == "markdown"
// In theory we should be sending lsp.MarkedString (for old clients)
// when len(cc.Hover.ContentFormat) == 0, but that's not possible
// without changing lsp.Hover.Content field type to interface{}
//
// We choose to follow gopls' approach (i.e. cut off old clients).
return &lsp.Hover{
Contents: markupContent(data.Content, mdSupported),
Range: HCLRangeToLSP(data.Range),
}
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package lsp
// LanguageID represents the coding language
// of a file
type LanguageID string
const (
HCL LanguageID = "hcl"
)
func (l LanguageID) String() string {
return string(l)
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package lsp
import (
"path/filepath"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/lang"
lsp "github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langserver/protocol"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/utils/uri"
"github.com/hashicorp/hcl/v2"
)
func ToLocationLink(path lang.Path, rng hcl.Range) lsp.LocationLink {
targetUri := uri.FromPath(filepath.Join(path.Path, rng.Filename))
lspRange := HCLRangeToLSP(rng)
locLink := lsp.LocationLink{
OriginSelectionRange: &lspRange,
TargetURI: lsp.DocumentURI(targetUri),
TargetRange: lspRange,
TargetSelectionRange: lspRange,
}
return locLink
}
func ToLocationLinks(path lang.Path, rng []hcl.Range) []lsp.LocationLink {
var ret []lsp.LocationLink
for _, r := range rng {
ret = append(ret, ToLocationLink(path, r))
}
return ret
}
func ToLocations(path lang.Path, rng []hcl.Range) []lsp.Location {
var ret []lsp.Location
for _, r := range rng {
ret = append(ret, ToLocation(path, r))
}
return ret
}
func ToLocation(path lang.Path, rng hcl.Range) lsp.Location {
targetUri := uri.FromPath(filepath.Join(path.Path, rng.Filename))
lspRange := HCLRangeToLSP(rng)
locLink := lsp.Location{
URI: lsp.DocumentURI(targetUri),
Range: lspRange,
}
return locLink
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package lsp
import (
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/lang"
lsp "github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langserver/protocol"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/utils/mdplain"
)
func markupContent(content lang.MarkupContent, mdSupported bool) lsp.MarkupContent {
value := content.Value()
kind := lsp.PlainText
if content.Kind() == lang.MarkdownKind {
if mdSupported {
kind = lsp.Markdown
} else {
value = mdplain.Clean(value)
}
}
return lsp.MarkupContent{
Kind: kind,
Value: value,
}
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package lsp
import (
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/document"
lsp "github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langserver/protocol"
"github.com/hashicorp/hcl/v2"
)
func HCLPositionFromLspPosition(pos lsp.Position, doc *document.Document) (hcl.Pos, error) {
byteOffset, err := document.ByteOffsetForPos(doc.Lines, lspPosToDocumentPos(pos))
if err != nil {
return hcl.Pos{}, err
}
return hcl.Pos{
Line: int(pos.Line) + 1,
Column: int(pos.Character) + 1,
Byte: byteOffset,
}, nil
}
func lspPosToDocumentPos(pos lsp.Position) document.Pos {
return document.Pos{
Line: int(pos.Line),
Column: int(pos.Character),
}
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package lsp
import (
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/document"
lsp "github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langserver/protocol"
"github.com/hashicorp/hcl/v2"
)
func documentRangeToLSP(docRng *document.Range) lsp.Range {
if docRng == nil {
return lsp.Range{}
}
return lsp.Range{
Start: lsp.Position{
Character: uint32(docRng.Start.Column),
Line: uint32(docRng.Start.Line),
},
End: lsp.Position{
Character: uint32(docRng.End.Column),
Line: uint32(docRng.End.Line),
},
}
}
func lspRangeToDocRange(rng *lsp.Range) *document.Range {
if rng == nil {
return nil
}
return &document.Range{
Start: document.Pos{
Line: int(rng.Start.Line),
Column: int(rng.Start.Character),
},
End: document.Pos{
Line: int(rng.End.Line),
Column: int(rng.End.Character),
},
}
}
func HCLRangeToLSP(rng hcl.Range) lsp.Range {
return lsp.Range{
Start: HCLPosToLSP(rng.Start),
End: HCLPosToLSP(rng.End),
}
}
func HCLPosToLSP(pos hcl.Pos) lsp.Position {
return lsp.Position{
Line: uint32(pos.Line - 1),
Character: uint32(pos.Column - 1),
}
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package lsp
import (
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/lang/semtok"
)
// Registering types which are actually in use
var (
serverTokenTypes = semtok.TokenTypes{
semtok.TokenTypeNamespace,
semtok.TokenTypeClass,
semtok.TokenTypeEnumMember,
semtok.TokenTypeFunction,
semtok.TokenTypeKeyword,
semtok.TokenTypeNumber,
semtok.TokenTypeProperty,
semtok.TokenTypeString,
semtok.TokenTypeVariable,
semtok.TokenTypeOperator,
}
serverTokenModifiers = semtok.TokenModifiers{
semtok.TokenModifierDeclaration,
semtok.TokenModifierDefinition,
}
)
func TokenTypesLegend() semtok.TokenTypes {
return serverTokenTypes
}
func TokenModifiersLegend() semtok.TokenModifiers {
return serverTokenModifiers
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package lsp
import (
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/lang"
lsp "github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langserver/protocol"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/utils/mdplain"
)
func ToSignatureHelp(signature *lang.FunctionSignature) *lsp.SignatureHelp {
if signature == nil {
return nil
}
parameters := make([]lsp.ParameterInformation, 0, len(signature.Parameters))
for _, p := range signature.Parameters {
parameters = append(parameters, lsp.ParameterInformation{
Label: p.Name,
// TODO: Support markdown per https://github.com/hashicorp/terraform-ls/issues/1212
Documentation: mdplain.Clean(p.Description.Value()),
})
}
return &lsp.SignatureHelp{
Signatures: []lsp.SignatureInformation{
{
Label: signature.Name,
// TODO: Support markdown per https://github.com/hashicorp/terraform-ls/issues/1212
Documentation: mdplain.Clean(signature.Description.Value()),
Parameters: parameters,
},
},
ActiveParameter: signature.ActiveParameter,
ActiveSignature: 0,
}
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package lsp
import (
"path/filepath"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/funchcl/decoder/symbols"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/lang"
lsp "github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langserver/protocol"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/utils/uri"
"github.com/zclconf/go-cty/cty"
)
// defaultSymbols is the list of symbols that were supported by the initial
// version of the LSP. This list is used as a fallback when the client does
// not provide a list of supported symbols.
var defaultSymbols = []lsp.SymbolKind{
lsp.File,
lsp.Module,
lsp.Namespace,
lsp.Package,
lsp.Class,
lsp.Method,
lsp.Property,
lsp.Field,
lsp.Constructor,
lsp.Enum,
lsp.Interface,
lsp.Function,
lsp.Variable,
lsp.Constant,
lsp.String,
lsp.Number,
lsp.Boolean,
lsp.Array,
}
func WorkspaceSymbols(sbs []symbols.Symbol, caps *lsp.WorkspaceSymbolClientCapabilities) []lsp.SymbolInformation {
syms := make([]lsp.SymbolInformation, len(sbs))
supportedSymbols := defaultSymbols
if caps != nil && caps.SymbolKind != nil {
supportedSymbols = caps.SymbolKind.ValueSet
}
for i, s := range sbs {
kind, ok := symbolKind(s, supportedSymbols)
if !ok {
// skip symbol not supported by client
continue
}
path := filepath.Join(s.Path().Path, s.Range().Filename)
syms[i] = lsp.SymbolInformation{
Name: s.Name(),
Kind: kind,
Location: lsp.Location{
Range: HCLRangeToLSP(s.Range()),
URI: lsp.DocumentURI(uri.FromPath(path)),
},
}
}
return syms
}
func DocumentSymbols(sbs []symbols.Symbol, caps lsp.DocumentSymbolClientCapabilities) []lsp.DocumentSymbol {
var syms []lsp.DocumentSymbol
for _, s := range sbs {
symbol, ok := documentSymbol(s, caps)
if !ok { // skip symbol not supported by client
continue
}
syms = append(syms, symbol)
}
return syms
}
func documentSymbol(symbol symbols.Symbol, caps lsp.DocumentSymbolClientCapabilities) (lsp.DocumentSymbol, bool) {
supportedSymbols := defaultSymbols
if caps.SymbolKind != nil {
supportedSymbols = caps.SymbolKind.ValueSet
}
kind, ok := symbolKind(symbol, supportedSymbols)
if !ok {
return lsp.DocumentSymbol{}, false
}
ds := lsp.DocumentSymbol{
Name: symbol.Name(),
Kind: kind,
Range: HCLRangeToLSP(symbol.Range()),
SelectionRange: HCLRangeToLSP(symbol.Range()),
}
if caps.HierarchicalDocumentSymbolSupport {
ds.Children = DocumentSymbols(symbol.NestedSymbols(), caps)
}
return ds, true
}
func symbolKind(symbol symbols.Symbol, supported []lsp.SymbolKind) (lsp.SymbolKind, bool) {
switch s := symbol.(type) {
case *symbols.BlockSymbol:
kind, ok := supportedSymbolKind(supported, lsp.Class)
if ok {
return kind, true
}
case *symbols.AttributeSymbol:
kind, ok := exprSymbolKind(s.ExprKind, supported)
if ok {
return kind, true
}
case *symbols.ExprSymbol:
kind, ok := exprSymbolKind(s.ExprKind, supported)
if ok {
return kind, true
}
}
return lsp.SymbolKind(0), false
}
func exprSymbolKind(symbolKind lang.SymbolExprKind, supported []lsp.SymbolKind) (lsp.SymbolKind, bool) {
switch k := symbolKind.(type) {
case lang.LiteralTypeKind:
switch k.Type {
case cty.Bool:
return supportedSymbolKind(supported, lsp.Boolean)
case cty.String:
return supportedSymbolKind(supported, lsp.String)
case cty.Number:
return supportedSymbolKind(supported, lsp.Number)
}
case lang.ReferenceExprKind:
return supportedSymbolKind(supported, lsp.Constant)
case lang.TupleConsExprKind:
return supportedSymbolKind(supported, lsp.Array)
case lang.ObjectConsExprKind:
return supportedSymbolKind(supported, lsp.Struct)
}
return supportedSymbolKind(supported, lsp.Variable)
}
func supportedSymbolKind(supported []lsp.SymbolKind, kind lsp.SymbolKind) (lsp.SymbolKind, bool) {
for _, s := range supported {
if s == kind {
return s, true
}
}
return lsp.SymbolKind(0), false
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package lsp
import (
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/document"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/lang"
lsp "github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langserver/protocol"
)
func TextEditsFromDocumentChanges(changes document.Changes) []lsp.TextEdit {
edits := make([]lsp.TextEdit, len(changes))
for i, change := range changes {
edits[i] = lsp.TextEdit{
Range: documentRangeToLSP(change.Range()),
NewText: change.Text(),
}
}
return edits
}
func TextEdits(tes []lang.TextEdit, snippetSupport bool) []lsp.TextEdit {
edits := make([]lsp.TextEdit, len(tes))
for i, te := range tes {
edits[i] = *textEdit(te, snippetSupport)
}
return edits
}
func textEdit(te lang.TextEdit, snippetSupport bool) *lsp.TextEdit {
if snippetSupport {
return &lsp.TextEdit{
NewText: te.Snippet,
Range: HCLRangeToLSP(te.Range),
}
}
return &lsp.TextEdit{
NewText: te.NewText,
Range: HCLRangeToLSP(te.Range),
}
}
func insertTextFormat(snippetSupport bool) lsp.InsertTextFormat {
if snippetSupport {
return lsp.SnippetTextFormat
}
return lsp.PlainTextTextFormat
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package lsp
import (
"bytes"
"log"
"math"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/document/source"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/lang/semtok"
)
type TokenEncoder struct {
types map[semtok.TokenType]int
mods map[semtok.TokenModifier]int
lines []source.Line
tokens []semtok.SemanticToken
lastEncodedTokenIdx int
}
func NewTokenEncoder(tokens []semtok.SemanticToken, lines []source.Line) *TokenEncoder {
types := map[semtok.TokenType]int{}
mods := map[semtok.TokenModifier]int{}
for i, tt := range TokenTypesLegend() {
types[tt] = i
}
for i, tm := range TokenModifiersLegend() {
mods[tm] = i
}
return &TokenEncoder{
types: types,
mods: mods,
tokens: tokens,
lines: lines,
}
}
func (te *TokenEncoder) Encode() []uint32 {
var data []uint32
for i := range te.tokens {
data = append(data, te.encodeTokenOfIndex(i)...)
}
return data
}
func computeBitmask(mapping map[semtok.TokenModifier]int, values semtok.TokenModifiers) int {
bitMask := 0b0
for _, modifier := range values {
index, ok := mapping[modifier]
if !ok {
log.Println("no mapping for token modifier:", modifier)
}
bitMask |= int(math.Pow(2, float64(index)))
}
return bitMask
}
func (te *TokenEncoder) encodeTokenOfIndex(i int) []uint32 {
var data []uint32
token := te.tokens[i]
tokenType := token.Type
tokenTypeIdx, ok := te.types[tokenType]
if !ok {
log.Println("no token type index for:", tokenType)
return data
}
modifierBitMask := computeBitmask(te.mods, token.Modifiers)
// Client may not support multiline tokens which would be indicated
// via lsp.SemanticTokensCapabilities.MultilineTokenSupport
// once it becomes available in gopls LSP structs.
//
// For now, we just safely assume client does *not* support it.
tokenLineDelta := token.Range.End.Line - token.Range.Start.Line
previousLine := 0
previousStartChar := 0
if i > 0 {
previousLine = te.tokens[te.lastEncodedTokenIdx].Range.End.Line - 1
currentLine := te.tokens[i].Range.End.Line - 1
if currentLine == previousLine {
previousStartChar = te.tokens[te.lastEncodedTokenIdx].Range.Start.Column - 1
}
}
if tokenLineDelta == 0 {
deltaLine := token.Range.Start.Line - 1 - previousLine
tokenLength := token.Range.End.Byte - token.Range.Start.Byte
deltaStartChar := token.Range.Start.Column - 1 - previousStartChar
data = append(data, []uint32{
uint32(deltaLine),
uint32(deltaStartChar),
uint32(tokenLength),
uint32(tokenTypeIdx),
uint32(modifierBitMask),
}...)
} else {
// Add entry for each line of a multiline token
for tokenLine := token.Range.Start.Line - 1; tokenLine <= token.Range.End.Line-1; tokenLine++ {
deltaLine := tokenLine - previousLine
deltaStartChar := 0
if tokenLine == token.Range.Start.Line-1 {
deltaStartChar = token.Range.Start.Column - 1 - previousStartChar
}
lineBytes := bytes.TrimRight(te.lines[tokenLine].Bytes, "\n\r")
length := len(lineBytes)
if tokenLine == token.Range.End.Line-1 {
length = token.Range.End.Column - 1
}
data = append(data, []uint32{
uint32(deltaLine),
uint32(deltaStartChar),
uint32(length),
uint32(tokenTypeIdx),
uint32(modifierBitMask),
}...)
previousLine = tokenLine
}
}
te.lastEncodedTokenIdx = i
return data
}
// Copyright 2022 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package protocol
import (
"encoding/json"
"fmt"
)
// DocumentChanges is a union of a file edit and directory rename operations
// for package renaming feature. At most one field of this struct is non-nil.
type DocumentChanges struct {
TextDocumentEdit *TextDocumentEdit
RenameFile *RenameFile
}
func (d *DocumentChanges) UnmarshalJSON(data []byte) error {
var m map[string]interface{}
if err := json.Unmarshal(data, &m); err != nil {
return err
}
if _, ok := m["textDocument"]; ok {
d.TextDocumentEdit = new(TextDocumentEdit)
return json.Unmarshal(data, d.TextDocumentEdit)
}
d.RenameFile = new(RenameFile)
return json.Unmarshal(data, d.RenameFile)
}
func (d *DocumentChanges) MarshalJSON() ([]byte, error) {
if d.TextDocumentEdit != nil {
return json.Marshal(d.TextDocumentEdit)
} else if d.RenameFile != nil {
return json.Marshal(d.RenameFile)
}
return nil, fmt.Errorf("empty DocumentChanges union value")
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package langserver
import (
"context"
"encoding/json"
"fmt"
"log"
"github.com/creachadair/jrpc2"
)
type rpcLogger struct {
logger *log.Logger
}
func (rl *rpcLogger) LogRequest(ctx context.Context, req *jrpc2.Request) {
idStr := ""
if req.ID() != "" {
idStr = fmt.Sprintf(" (ID %s)", req.ID())
}
reqType := "request"
if req.IsNotification() {
reqType = "notification"
}
var params json.RawMessage
_ = req.UnmarshalParams(¶ms)
rl.logger.Printf("Incoming %s for %q%s: %s",
reqType, req.Method(), idStr, params)
}
func (rl *rpcLogger) LogResponse(ctx context.Context, rsp *jrpc2.Response) {
idStr := ""
if rsp.ID() != "" {
idStr = fmt.Sprintf(" (ID %s)", rsp.ID())
}
req := jrpc2.InboundRequest(ctx)
if req.IsNotification() {
idStr = " (notification)"
}
if rsp.Error() != nil {
rl.logger.Printf("Error for %q%s: %s", req.Method(), idStr, rsp.Error())
return
}
// var body json.RawMessage
// rsp.UnmarshalResult(&body)
// rl.logger.Printf("Response to %q%s: %s", req.Method(), idStr, body)
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package session
import (
"fmt"
"github.com/creachadair/jrpc2"
)
const NotInitialized jrpc2.Code = -32002
type unexpectedState struct {
want state
have state
}
func (e *unexpectedState) Error() string {
return fmt.Sprintf("session is not %s, current state: %s",
e.want, e.have)
}
func notInitializedErr(state state) error {
uss := &unexpectedState{
want: stateInitializedConfirmed,
have: state,
}
if state < stateInitializedConfirmed {
return fmt.Errorf("%w: %s", NotInitialized.Err(), uss)
}
if state == stateDown {
return fmt.Errorf("%w: %s", jrpc2.InvalidRequest.Err(), uss)
}
return uss
}
func alreadyInitializedErr(reqID string) error {
return fmt.Errorf("%w: session was already initialized via request ID %s",
jrpc2.SystemError.Err(), reqID)
}
func alreadyDownErr(reqID string) error {
return fmt.Errorf("%w: session was already shut down via request %s",
jrpc2.InvalidRequest.Err(), reqID)
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
// Package session provides session lifecycle tracking for a language server.
package session
import (
"context"
"fmt"
"time"
"github.com/creachadair/jrpc2"
)
type Lifecycle struct {
initializeReq *jrpc2.Request
initializeReqTime time.Time
initializedReq *jrpc2.Request
initializedReqTime time.Time
downReq *jrpc2.Request
downReqTime time.Time
state state
exitFunc context.CancelFunc
}
func (s *Lifecycle) isPrepared() bool {
return s.state == statePrepared
}
func (s *Lifecycle) Prepare() error {
if s.state != stateEmpty {
return &unexpectedState{
want: stateInitializedConfirmed,
have: s.state,
}
}
s.state = statePrepared
return nil
}
func (s *Lifecycle) IsInitializedUnconfirmed() bool {
return s.state == stateInitializedUnconfirmed
}
func (s *Lifecycle) Initialize(req *jrpc2.Request) error {
if s.state != statePrepared {
if s.IsInitializedUnconfirmed() {
return alreadyInitializedErr(s.initializeReq.ID())
}
return fmt.Errorf("session is not ready to be initialized. state: %s",
s.state)
}
s.initializeReq = req
s.initializeReqTime = time.Now()
s.state = stateInitializedUnconfirmed
return nil
}
func (s *Lifecycle) isInitializationConfirmed() bool {
return s.state == stateInitializedConfirmed
}
func (s *Lifecycle) CheckInitializationIsConfirmed() error {
if !s.isInitializationConfirmed() {
return notInitializedErr(s.state)
}
return nil
}
func (s *Lifecycle) ConfirmInitialization(req *jrpc2.Request) error {
if s.state != stateInitializedUnconfirmed {
if s.isInitializationConfirmed() {
return fmt.Errorf("session was already confirmed as initalized at %s via request %s",
s.initializedReqTime, s.initializedReq.ID())
}
return fmt.Errorf("session is not ready to be confirmed as initialized (%s)",
s.state)
}
s.initializedReq = req
s.initializedReqTime = time.Now()
s.state = stateInitializedConfirmed
return nil
}
func (s *Lifecycle) Shutdown(req *jrpc2.Request) error {
if s.isDown() {
return alreadyDownErr(s.downReq.ID())
}
s.downReq = req
s.downReqTime = time.Now()
s.state = stateDown
return nil
}
func (s *Lifecycle) Exit() error {
if !s.isExitable() {
return fmt.Errorf("cannot exit as session is %s", s.state)
}
s.exitFunc()
return nil
}
func (s *Lifecycle) isExitable() bool {
return s.isDown() || s.isPrepared()
}
func (s *Lifecycle) isDown() bool {
return s.state == stateDown
}
func NewLifecycle(exitFunc context.CancelFunc) *Lifecycle {
return &Lifecycle{
state: stateEmpty,
exitFunc: exitFunc,
}
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package session
// state represents state of the language server
// workspace ("session") with respect to the LSP
type state int
const (
stateEmpty state = -1 // before session starts
statePrepared state = 0 // after session starts, before any request
stateInitializedUnconfirmed state = 1 // after "initialize", before "initialized"
stateInitializedConfirmed state = 2 // after "initialized"
stateDown state = 3 // after "shutdown"
)
func (ss state) String() string {
switch ss {
case stateEmpty:
return "<empty>"
case statePrepared:
return "prepared"
case stateInitializedUnconfirmed:
return "initialized (unconfirmed)"
case stateInitializedConfirmed:
return "initialized (confirmed)"
case stateDown:
return "down"
}
return "<unknown>"
}
// Package resource provides facilities to convert CRDs and XRDs to HCL schemas.
package resource
import (
"fmt"
"log"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/schema"
xpv1 "github.com/crossplane/crossplane/v2/apis/apiextensions/v1"
v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apimachinery/pkg/runtime"
)
// Scope is the scope for which CRDs are loaded. This can be "cluster", "namespaced" or
// "both".
type Scope string
const (
ScopeNamespaced Scope = "namespaced"
ScopeCluster Scope = "cluster"
ScopeBoth Scope = "both"
)
// Key is the key of a K8s object.
type Key struct {
ApiVersion string
Kind string
}
func (k Key) String() string {
return fmt.Sprintf("%s %s", k.Kind, k.ApiVersion)
}
// ScopedAttributeSchema wraps an attribute schema and also tracks the scope for the top-level objects.
type ScopedAttributeSchema struct {
schema.AttributeSchema
Scope Scope
}
// Schemas maintains attribute schemas for a set of K8s types.
type Schemas struct {
s map[Key]*ScopedAttributeSchema
l []Key
}
// Keys returns the keys known to this instance.
func (s *Schemas) Keys() []Key { return s.l }
// Schema returns the attribute schema for the specified API version and kind,
// or nil if a schema for this key could not be found.
func (s *Schemas) Schema(apiVersion, kind string) *schema.AttributeSchema {
as := s.s[Key{ApiVersion: apiVersion, Kind: kind}]
if as == nil {
return nil
}
return &as.AttributeSchema
}
// FilterScope returns schemas filtered by scope.
func (s *Schemas) FilterScope(scope Scope) *Schemas {
if scope != ScopeNamespaced && scope != ScopeCluster {
return s
}
m := map[Key]*ScopedAttributeSchema{}
for k, v := range s.s {
if v.Scope == scope {
m[k] = v
}
}
return newSchemas(m)
}
// ToSchemas converts CRDs and XRDs present in the supplied objects into a Schemas instance.
func ToSchemas(objects ...runtime.Object) *Schemas {
ret := map[Key]*ScopedAttributeSchema{}
for _, o := range objects {
switch c := o.(type) {
case *v1.CustomResourceDefinition:
aggregateCRDToMap(c, ret)
case *xpv1.CompositeResourceDefinition:
aggregateXRDToMap(c, ret)
}
}
return newSchemas(ret)
}
// Compose composes the supplied schemas into a singular instance.
func Compose(schemas ...*Schemas) *Schemas {
out := map[Key]*ScopedAttributeSchema{}
for _, s := range schemas {
for k, v := range s.s {
_, seen := out[k]
if seen {
log.Printf("multiple schemas found for %v", k)
continue
}
out[k] = v
}
}
return newSchemas(out)
}
package loader
import (
"os"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/resource"
)
// File loads schemas from YAML files.
type File struct {
file string
}
// NewFile returns a loader that loads schemas from a YAML file that
// can contains multiple YAML documents each representing a CRD.
func NewFile(file string) *File {
return &File{file: file}
}
// Load returns schemas found in the YAML file.
func (f *File) Load() (*resource.Schemas, error) {
fh, err := os.Open(f.file)
if err != nil {
return nil, err
}
defer func() { _ = fh.Close() }()
return LoadReader(fh)
}
package loader
import (
"archive/tar"
"io"
"path/filepath"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/resource"
"github.com/google/go-containerregistry/pkg/crane"
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/mutate"
"github.com/google/go-containerregistry/pkg/v1/validate"
"github.com/pkg/errors"
"k8s.io/apimachinery/pkg/runtime"
)
const (
packageFile = "package.yaml"
errBadReference = "package tag is not a valid reference"
errFetchPackage = "failed to fetch package from remote"
errGetManifest = "failed to get package image manifest from remote"
errFetchLayer = "failed to fetch annotated base layer from remote"
errGetUncompressed = "failed to get uncompressed contents from layer"
errMultipleAnnotatedLayers = "package is invalid due to multiple annotated base layers"
errFmtNoPackageFileFound = "couldn't find \"" + packageFile + "\" file after checking %d files in the archive (annotated layer: %v)"
errFmtMaxManifestLayers = "package has %d layers, but only %d are allowed"
errValidateLayer = "invalid package layer"
errValidateImage = "invalid package image"
)
const (
layerAnnotation = "io.crossplane.xpkg"
baseAnnotationValue = "base"
// maxLayers is the maximum number of layers an image can have.
maxLayers = 256
)
// CrossplanePackage loads schemas from crossplane packages gotten from an OCI registry.
type CrossplanePackage struct {
imageRef string
}
// NewCrossplanePackage creates a CrossplanePackage
func NewCrossplanePackage(imageRef string) *CrossplanePackage {
return &CrossplanePackage{imageRef: imageRef}
}
func (p *CrossplanePackage) Load() (*resource.Schemas, error) {
objs, err := p.ExtractObjects()
if err != nil {
return nil, err
}
return resource.ToSchemas(objs...), nil
}
func (p *CrossplanePackage) ExtractObjects() ([]runtime.Object, error) {
ref, err := name.ParseReference(p.imageRef)
if err != nil {
return nil, errors.Wrap(err, errBadReference)
}
img, err := crane.Pull(ref.String())
if err != nil {
return nil, errors.Wrap(err, errFetchPackage)
}
rc, err := p.getPackageYamlStream(img)
if err != nil {
return nil, err
}
defer func() { _ = rc.Close() }()
return ExtractObjects(rc)
}
// getPackageYamlStream extracts the package YAML stream from the downloaded image.
// Code copied from the crossplane source.
func (p *CrossplanePackage) getPackageYamlStream(img v1.Image) (io.ReadCloser, error) {
// Get image manifest.
manifest, err := img.Manifest()
if err != nil {
return nil, errors.Wrap(err, errGetManifest)
}
// Check that the image has less than the maximum allowed number of layers.
if nLayers := len(manifest.Layers); nLayers > maxLayers {
return nil, errors.Errorf(errFmtMaxManifestLayers, nLayers, maxLayers)
}
var tarc io.ReadCloser
// determine if the image is using annotated layers.
foundAnnotated := false
for _, l := range manifest.Layers {
if a, ok := l.Annotations[layerAnnotation]; !ok || a != baseAnnotationValue {
continue
}
if foundAnnotated {
return nil, errors.New(errMultipleAnnotatedLayers)
}
foundAnnotated = true
layer, err := img.LayerByDigest(l.Digest)
if err != nil {
return nil, errors.Wrap(err, errFetchLayer)
}
if err := validate.Layer(layer); err != nil {
return nil, errors.Wrap(err, errValidateLayer)
}
tarc, err = layer.Uncompressed()
if err != nil {
return nil, errors.Wrap(err, errGetUncompressed)
}
}
// If we still don't have content then we need to flatten image filesystem.
if !foundAnnotated {
if err := validate.Image(img); err != nil {
return nil, errors.Wrap(err, errValidateImage)
}
tarc = mutate.Extract(img)
}
// the ReadCloser is an uncompressed tarball, either consisting of annotated
// layer contents or flattened filesystem content. Either way, we only want
// the package YAML stream.
t := tar.NewReader(tarc)
var read int
for {
h, err := t.Next()
if err != nil {
return nil, errors.Wrapf(err, errFmtNoPackageFileFound, read, foundAnnotated)
}
if filepath.Base(h.Name) == packageFile {
break
}
read++
}
return &joinedReadCloser{r: t, c: tarc}, nil
}
// joinedReadCloser joins a reader and a closer. It is typically used in the
// context of a ReadCloser being wrapped by a Reader.
type joinedReadCloser struct {
r io.Reader
c io.Closer
}
// Read calls the underlying reader Read method.
func (r *joinedReadCloser) Read(b []byte) (int, error) {
return r.r.Read(b)
}
// Close closes the closer for the JoinedReadCloser.
func (r *joinedReadCloser) Close() error {
return r.c.Close()
}
/*
Copyright 2020 The Crossplane Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package loader
import (
"context"
"fmt"
"io"
"github.com/crossplane/crossplane-runtime/v2/pkg/parser"
v1 "github.com/crossplane/crossplane/v2/apis/apiextensions/v1"
"github.com/crossplane/crossplane/v2/apis/apiextensions/v1alpha1"
v2 "github.com/crossplane/crossplane/v2/apis/apiextensions/v2"
opsv1alpha1 "github.com/crossplane/crossplane/v2/apis/ops/v1alpha1"
pkgmetav1 "github.com/crossplane/crossplane/v2/apis/pkg/meta/v1"
pkgmetav1alpha1 "github.com/crossplane/crossplane/v2/apis/pkg/meta/v1alpha1"
pkgmetav1beta1 "github.com/crossplane/crossplane/v2/apis/pkg/meta/v1beta1"
admv1 "k8s.io/api/admissionregistration/v1"
extv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
extv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
"k8s.io/apimachinery/pkg/runtime"
)
// buildMetaScheme builds the default scheme used for identifying metadata in a
// Crossplane package.
func buildMetaScheme() (*runtime.Scheme, error) {
metaScheme := runtime.NewScheme()
if err := pkgmetav1alpha1.SchemeBuilder.AddToScheme(metaScheme); err != nil {
return nil, err
}
if err := pkgmetav1beta1.SchemeBuilder.AddToScheme(metaScheme); err != nil {
return nil, err
}
if err := pkgmetav1.SchemeBuilder.AddToScheme(metaScheme); err != nil {
return nil, err
}
return metaScheme, nil
}
// buildObjectScheme builds the default scheme used for identifying objects in a
// Crossplane package.
func buildObjectScheme() (*runtime.Scheme, error) {
objScheme := runtime.NewScheme()
if err := v1.AddToScheme(objScheme); err != nil {
return nil, err
}
if err := v1alpha1.AddToScheme(objScheme); err != nil {
return nil, err
}
if err := opsv1alpha1.AddToScheme(objScheme); err != nil {
return nil, err
}
if err := v2.AddToScheme(objScheme); err != nil {
return nil, err
}
if err := extv1beta1.AddToScheme(objScheme); err != nil {
return nil, err
}
if err := extv1.AddToScheme(objScheme); err != nil {
return nil, err
}
if err := admv1.AddToScheme(objScheme); err != nil {
return nil, err
}
return objScheme, nil
}
func extractObjects(rc io.ReadCloser) ([]runtime.Object, error) {
metaScheme, err := buildMetaScheme()
if err != nil {
return nil, err
}
objScheme, err := buildObjectScheme()
if err != nil {
return nil, err
}
// parse package using Crossplane's parser
pkg, err := parser.New(metaScheme, objScheme).Parse(context.Background(), rc)
if err != nil {
return nil, fmt.Errorf("failed to parse package: %w", err)
}
return pkg.GetObjects(), nil
}
package loader
import (
"io"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/resource"
"k8s.io/apimachinery/pkg/runtime"
)
// ExtractObjects returns runtime objects from the supplied reader that is assumed
// to have one or more YAML documents.
func ExtractObjects(reader io.Reader) ([]runtime.Object, error) {
return extractObjects(io.NopCloser(reader))
}
// LoadReader returns schemas found in the supplied reader.
func LoadReader(reader io.Reader) (*resource.Schemas, error) {
objs, err := ExtractObjects(reader)
if err != nil {
return nil, err
}
return resource.ToSchemas(objs...), nil
}
package resource
import (
"encoding/json"
"fmt"
"log"
"sort"
ourschema "github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/funchcl/schema"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/lang"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/langhcl/schema"
xpv1 "github.com/crossplane/crossplane/v2/apis/apiextensions/v1"
v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
)
// newSchemas returns a schemas instance given existing mappings.
func newSchemas(m map[Key]*ScopedAttributeSchema) *Schemas {
s := &Schemas{s: m, l: make([]Key, 0, len(m))}
for k := range m {
s.l = append(s.l, k)
}
sort.Slice(s.l, func(i, j int) bool {
if s.l[i].Kind == s.l[j].Kind {
return s.l[i].ApiVersion < s.l[j].ApiVersion
}
return s.l[i].Kind < s.l[j].Kind
})
return s
}
func aggregateCRDToMap(crd *v1.CustomResourceDefinition, ret map[Key]*ScopedAttributeSchema) {
group := crd.Spec.Group
kind := crd.Spec.Names.Kind
for _, versionDef := range crd.Spec.Versions {
version := versionDef.Name
key := Key{ApiVersion: group + "/" + version, Kind: kind}
attrSchema := toAttributeSchema(versionDef.Schema.OpenAPIV3Schema, true)
scope := ScopeCluster
if crd.Spec.Scope == v1.NamespaceScoped {
scope = ScopeNamespaced
}
typeName := fmt.Sprintf("%s:%s/%s", kind, group, version)
ret[key] = &ScopedAttributeSchema{
Scope: scope,
AttributeSchema: schema.AttributeSchema{
Description: lang.PlainText(versionDef.Schema.OpenAPIV3Schema.Description),
Constraint: schema.Object{
Attributes: attrSchema,
Name: typeName,
Description: lang.PlainText(versionDef.Schema.OpenAPIV3Schema.Description),
},
},
}
}
}
func aggregateXRDToMap(crd *xpv1.CompositeResourceDefinition, ret map[Key]*ScopedAttributeSchema) {
group := crd.Spec.Group
kind := crd.Spec.Names.Kind
for _, versionDef := range crd.Spec.Versions {
version := versionDef.Name
key := Key{ApiVersion: group + "/" + version, Kind: kind}
var props v1.JSONSchemaProps
err := json.Unmarshal(versionDef.Schema.OpenAPIV3Schema.Raw, &props)
if err != nil {
log.Printf("unexpected error unmarshalling xrd: %v", err)
return
}
scope := ScopeCluster
if crd.Spec.Scope != nil && *crd.Spec.Scope == xpv1.CompositeResourceScopeNamespaced {
scope = ScopeNamespaced
}
attrSchema := toAttributeSchema(&props, true)
typeName := fmt.Sprintf("%s:%s/%s", kind, group, version)
ret[key] = &ScopedAttributeSchema{
Scope: scope,
AttributeSchema: schema.AttributeSchema{
Description: lang.PlainText(props.Description),
Constraint: schema.Object{
Attributes: attrSchema,
Name: typeName,
Description: lang.PlainText(""),
},
},
}
}
}
func isRequired(requiredProps []string, name string) bool {
for _, prop := range requiredProps {
if prop == name {
return true
}
}
return false
}
var k8sSchema = ourschema.BasicK8sObjectConstraint()
func toAttributeSchema(obj *v1.JSONSchemaProps, topLevel bool) schema.ObjectAttributes {
if obj == nil || obj.Type != "object" {
panic("invalid call to toAttributeSchema")
}
attrs := schema.ObjectAttributes{}
if topLevel {
for name, attr := range k8sSchema.Attributes {
attrs[name] = attr
}
}
for name, props := range obj.Properties {
// prefer pre-filled attribute defs rather than using the ones supplied
if _, ok := attrs[name]; ok {
continue
}
required := isRequired(obj.Required, name)
def := &schema.AttributeSchema{
Description: lang.PlainText(props.Description),
IsRequired: required,
IsOptional: !required,
// Default: TODO
Constraint: constraintFor(&props),
}
attrs[name] = def
}
return attrs
}
var defaultConstraint = schema.String{}
func constraintFor(props *v1.JSONSchemaProps) schema.Constraint {
if props == nil {
log.Println("no props!")
return defaultConstraint
}
switch props.Type {
case "object":
if len(props.Properties) == 0 && props.AdditionalProperties != nil && props.AdditionalProperties.Schema != nil {
return schema.Map{Elem: constraintFor(props.AdditionalProperties.Schema)}
}
attrs := toAttributeSchema(props, false)
return schema.Object{
Attributes: attrs,
AllowInterpolatedKeys: true,
}
case "array":
childConstraint := constraintFor(props.Items.Schema)
return schema.List{Elem: childConstraint}
case "string":
return schema.String{}
case "integer", "number":
return schema.Number{}
case "boolean":
return schema.Bool{}
}
return defaultConstraint
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
// Package logging provider logging facilities.
package logging
import (
"fmt"
"io"
"log"
"os"
"path/filepath"
"strings"
"text/template"
)
// PerfLogger is a nil logger than can be set to a real one to log timing information from multiple places.
var PerfLogger *log.Logger = NopLogger()
func NopLogger() *log.Logger {
return log.New(io.Discard, "", 0)
}
func NewLogger(w io.Writer) *log.Logger {
return log.New(w, "", log.LstdFlags|log.Lshortfile)
}
// FileLogger wraps a file-based logger.
type FileLogger struct {
l *log.Logger
f *os.File
}
// NewFileLogger creates a new file-based logger.
func NewFileLogger(rawPath string) (*FileLogger, error) {
path, err := parseLogPath(rawPath)
if err != nil {
return nil, fmt.Errorf("failed to parse path: %w", err)
}
if !filepath.IsAbs(path) {
return nil, fmt.Errorf("please provide absolute log path to prevent ambiguity (given: %q)",
path)
}
mode := os.O_TRUNC | os.O_CREATE | os.O_WRONLY
file, err := os.OpenFile(path, mode, 0o600)
if err != nil {
return nil, err
}
return &FileLogger{
l: NewLogger(file),
f: file,
}, nil
}
// Writer returns the underlying io.Writer for the file logger.
func (fl *FileLogger) Writer() io.Writer {
return fl.f
}
func parseLogPath(rawPath string) (string, error) {
tpl, err := newPath("log-file").Parse(rawPath)
if err != nil {
return "", err
}
buf := &strings.Builder{}
err = tpl.Execute(buf, nil)
if err != nil {
return "", err
}
return buf.String(), nil
}
func ValidateExecLogPath(rawPath string) error {
_, err := parseExecLogPathTemplate("", rawPath)
return err
}
func ParseExecLogPath(method string, rawPath string) (string, error) {
tpl, err := parseExecLogPathTemplate(method, rawPath)
if err != nil {
return "", err
}
buf := &strings.Builder{}
err = tpl.Execute(buf, nil)
if err != nil {
return "", err
}
return buf.String(), nil
}
func parseExecLogPathTemplate(method string, rawPath string) (templatedPath, error) {
tpl := newPath("tf-log-file")
methodFunc := func() string {
return method
}
tpl = tpl.Funcs(template.FuncMap{
"method": methodFunc,
// DEPRECATED
"args": methodFunc,
})
return tpl.Parse(rawPath)
}
// Logger returns the underlying logger.
func (fl *FileLogger) Logger() *log.Logger {
return fl.l
}
// Close closes the log file.
func (fl *FileLogger) Close() error {
return fl.f.Close()
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package logging
import (
"io"
"os"
"strings"
"text/template"
"time"
)
type templatedPath interface {
Parse(text string) (*template.Template, error)
Funcs(funcMap template.FuncMap) *template.Template
Execute(wr io.Writer, data interface{}) error
}
func newPath(name string) templatedPath {
tpl := template.New(name)
tpl = tpl.Funcs(template.FuncMap{
"timestamp": time.Now().Local().Unix,
"pid": os.Getpid,
"ppid": os.Getppid,
})
return tpl
}
func ParseRawPath(name string, rawPath string) (string, error) {
tpl, err := newPath(name).Parse(rawPath)
if err != nil {
return "", err
}
buf := &strings.Builder{}
err = tpl.Execute(buf, nil)
if err != nil {
return "", err
}
return buf.String(), nil
}
package logging
import (
"io"
"log"
"strings"
"sync"
)
// Module represents a loggable component in the system.
type Module string
const (
ModuleLangServer Module = "langserver"
ModuleHandlers Module = "handlers"
ModuleEventBus Module = "eventbus"
ModuleModules Module = "modules"
ModuleCRDs Module = "crds"
ModuleFilesystem Module = "filesystem"
ModuleDocStore Module = "docstore"
ModulePerf Module = "perf"
)
// AllModules lists all available modules for logging.
var AllModules = []Module{
ModuleLangServer,
ModuleHandlers,
ModuleEventBus,
ModuleModules,
ModuleCRDs,
ModuleFilesystem,
ModuleDocStore,
ModulePerf,
}
// registry manages module logging configuration.
type registry struct {
mu sync.RWMutex
enabledModules map[Module]bool
output io.Writer
loggers map[Module]*log.Logger
}
var globalRegistry = ®istry{
enabledModules: make(map[Module]bool),
loggers: make(map[Module]*log.Logger),
}
// Init initializes the logging registry with the given output and enabled modules.
// This should be called once at startup before any components are created.
func Init(output io.Writer, enabledModules []Module) {
globalRegistry.mu.Lock()
defer globalRegistry.mu.Unlock()
globalRegistry.output = output
globalRegistry.enabledModules = make(map[Module]bool)
globalRegistry.loggers = make(map[Module]*log.Logger)
for _, m := range enabledModules {
globalRegistry.enabledModules[m] = true
}
// Update PerfLogger based on whether perf module is enabled
if globalRegistry.enabledModules[ModulePerf] && output != nil {
PerfLogger = NewLogger(output)
} else {
PerfLogger = NopLogger()
}
}
// LoggerFor returns a logger for the specified module.
// If the module is enabled, it returns a real logger; otherwise, it returns a NopLogger.
func LoggerFor(module Module) *log.Logger {
globalRegistry.mu.RLock()
if logger, ok := globalRegistry.loggers[module]; ok {
globalRegistry.mu.RUnlock()
return logger
}
globalRegistry.mu.RUnlock()
globalRegistry.mu.Lock()
defer globalRegistry.mu.Unlock()
// Double-check after acquiring write lock
if logger, ok := globalRegistry.loggers[module]; ok {
return logger
}
var logger *log.Logger
if globalRegistry.enabledModules[module] && globalRegistry.output != nil {
logger = log.New(globalRegistry.output, "["+string(module)+"] ", log.LstdFlags|log.Lshortfile)
} else {
logger = NopLogger()
}
globalRegistry.loggers[module] = logger
return logger
}
// ParseModules parses a comma-separated list of module names.
// The special value "all" enables all modules.
func ParseModules(input string) ([]Module, error) {
if input == "" {
return nil, nil
}
if input == "all" {
return AllModules, nil
}
parts := strings.Split(input, ",")
modules := make([]Module, 0, len(parts))
validModules := make(map[Module]bool)
for _, m := range AllModules {
validModules[m] = true
}
for _, p := range parts {
p = strings.TrimSpace(p)
if p == "" {
continue
}
m := Module(p)
if !validModules[m] {
// Return list of valid modules in error
validNames := make([]string, len(AllModules))
for i, vm := range AllModules {
validNames[i] = string(vm)
}
return nil, &InvalidModuleError{
Module: p,
ValidModules: validNames,
}
}
modules = append(modules, m)
}
return modules, nil
}
// InvalidModuleError is returned when an invalid module name is specified.
type InvalidModuleError struct {
Module string
ValidModules []string
}
func (e *InvalidModuleError) Error() string {
return "invalid log module: " + e.Module + " (valid modules: " + strings.Join(e.ValidModules, ", ") + ")"
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
// Package mdplain allows cleanup of markdown into plain text.
package mdplain
import (
"regexp"
)
type replacement struct {
re *regexp.Regexp
sub string
}
var replacements = []replacement{
// rules heavily inspired by: https://github.com/stiang/remove-markdown/blob/master/index.js
// back references were removed
// Header
{regexp.MustCompile(`\n={2,}`), "\n"},
// Fenced codeblocks
{regexp.MustCompile(`~{3}.*\n`), ""},
// Strikethrough
{regexp.MustCompile("~~"), ""},
// Fenced codeblocks
{regexp.MustCompile("`{3}.*\\n"), ""},
// Remove HTML tags
{regexp.MustCompile(`<[^>]*>`), ""},
// Remove setext-style headers
{regexp.MustCompile(`^[=\-]{2,}\s*$`), ""},
// Remove footnotes?
{regexp.MustCompile(`\[\^.+?\](\: .*?$)?`), ""},
{regexp.MustCompile(`\s{0,2}\[.*?\]: .*?$`), ""},
// Remove images
{regexp.MustCompile(`\!\[(.*?)\][\[\(].*?[\]\)]`), "$1"},
// Remove inline links
{regexp.MustCompile(`\[(.*?)\][\[\(].*?[\]\)]`), "$1"},
// Remove blockquotes
{regexp.MustCompile(`^\s{0,3}>\s?`), ""},
// Remove reference-style links?
{regexp.MustCompile(`^\s{1,2}\[(.*?)\]: (\S+)( ".*?")?\s*$`), ""},
// Remove atx-style headers
{regexp.MustCompile(`^(\n)?\s{0,}#{1,6}\s+| {0,}(\n)?\s{0,}#{0,} {0,}(\n)?\s{0,}$`), "$1$2$3"},
// Remove emphasis (repeat the line to remove double emphasis)
{regexp.MustCompile(`([*_]{1,3})([^\t\n\f\r *_].*?[^\t\n\f\r *_]{0,1})([*_]{1,3})`), "$2"},
{regexp.MustCompile(`([*_]{1,3})([^\t\n\f\r *_].*?[^\t\n\f\r *_]{0,1})([*_]{1,3})`), "$2"},
// Remove code blocks
{regexp.MustCompile("(`{3,})(.*?)(`{3,})"), "$2"},
// Remove inline code
{regexp.MustCompile("`(.+?)`"), "$1"},
// Replace two or more newlines with exactly two? Not entirely sure this belongs here...
{regexp.MustCompile(`\n{2,}`), "\n\n"},
}
// Clean runs a VERY naive cleanup of Markdown text to make it more palatable as plain text.
func Clean(markdown string) string {
// TODO: maybe use https://github.com/russross/blackfriday/tree/v2, write custom renderer or
// generate HTML then process that to plaintext using https://github.com/jaytaylor/html2text
for _, r := range replacements {
markdown = r.re.ReplaceAllString(markdown, r.sub)
}
return markdown
}
// Package perf provides facilities to measure and report performance
package perf
import (
"time"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/utils/logging"
)
// Measure tracks the operation to be measured and returns a function that can be
// run in a defer block of the caller to output the time taken for the operation.
func Measure(s string) func() {
if logging.PerfLogger == nil {
return func() {}
}
start := time.Now()
return func() {
logging.PerfLogger.Printf("%s [%s]", s, time.Since(start))
}
}
// Package queue provides a simple queue implementation with support for
// deduping equivalent jobs using the notion of a unique key for each job.
package queue
import (
"context"
"log"
"sync"
"sync/atomic"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/utils/perf"
)
// Key is a unique key for a job.
type Key string
func (k Key) String() string {
return string(k)
}
type job struct {
id uint64
key Key
fn func() error
l sync.Mutex
canceled bool
}
func (j *job) setCanceled(flag bool) {
j.l.Lock()
defer j.l.Unlock()
j.canceled = flag
}
func (j *job) isCanceled() bool {
j.l.Lock()
defer j.l.Unlock()
return j.canceled
}
type waiter struct {
ids map[uint64]struct{}
ch chan struct{}
}
// Queue implements the queue.
type Queue struct {
l sync.Mutex
concurrency int
nextID uint64
work chan *job
pendingJobsByKey map[Key]*job
runningJobsByKey map[Key]*job
waiters []waiter
}
// New creates a queue with the supplied concurrency.
func New(concurrency int) *Queue {
return &Queue{
concurrency: concurrency,
work: make(chan *job, 1000),
pendingJobsByKey: map[Key]*job{},
runningJobsByKey: map[Key]*job{},
}
}
// Start starts the background processing for the queue.
// Processing stops when the context is canceled.
func (q *Queue) Start(ctx context.Context) {
processItem := func(j *job) {
defer perf.Measure("job: " + j.key.String())()
q.setProcessing(j)
defer q.setDone(j)
if !j.isCanceled() {
err := j.fn()
if err != nil {
log.Printf("Error in job %s [%d]: %v", j.key, j.id, err)
}
}
}
for i := 0; i < q.concurrency; i++ {
go func() {
for {
select {
case j := <-q.work:
processItem(j)
case <-ctx.Done():
return
}
}
}()
}
}
func (q *Queue) setProcessing(j *job) {
q.l.Lock()
defer q.l.Unlock()
delete(q.pendingJobsByKey, j.key)
q.runningJobsByKey[j.key] = j
}
func (q *Queue) setDone(j *job) {
q.l.Lock()
defer q.l.Unlock()
delete(q.runningJobsByKey, j.key)
newWaiters := make([]waiter, 0, len(q.waiters))
for _, w := range q.waiters {
delete(w.ids, j.id)
if len(w.ids) == 0 {
close(w.ch)
continue
}
newWaiters = append(newWaiters, w)
}
q.waiters = newWaiters
}
// Enqueue enqueues a job with a specific key. The job will not be added to the queue
// if there already exists a job that is waiting to be processed which has the same key.
// Note that a running job for the same key will still cause this one to be enqueued.
func (q *Queue) Enqueue(key Key, fn func() error) {
q.l.Lock()
defer q.l.Unlock()
j := q.pendingJobsByKey[key]
if j != nil {
j.setCanceled(false)
return
}
j = &job{
id: atomic.AddUint64(&q.nextID, 1),
key: key,
fn: fn,
}
q.pendingJobsByKey[j.key] = j
q.work <- j
}
// Dequeue removes any waiting job having the supplied key. Running jobs are not affected.
func (q *Queue) Dequeue(key Key) {
q.l.Lock()
defer q.l.Unlock()
j := q.pendingJobsByKey[key]
if j != nil {
j.setCanceled(true)
}
}
// WaitForKey waits until the running and waiting jobs for this key have completed.
// Note that it will not wait forever even the queue keeps getting new job with the same key
// after this function is called.
func (q *Queue) WaitForKey(key Key) {
q.l.Lock()
j1 := q.runningJobsByKey[key]
j2 := q.pendingJobsByKey[key]
if j1 == nil && j2 == nil {
q.l.Unlock()
return
}
ch := q.addWaiter(j1, j2)
q.l.Unlock()
<-ch
}
func (q *Queue) addWaiter(j1, j2 *job) <-chan struct{} {
ch := make(chan struct{})
m := map[uint64]struct{}{}
if j1 != nil {
m[j1.id] = struct{}{}
}
if j2 != nil {
m[j2.id] = struct{}{}
}
w := waiter{
ids: m,
ch: ch,
}
q.waiters = append(q.waiters, w)
return ch
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
// Package uri provides miscellaneous facilities for URI manipulation.
package uri
import (
"fmt"
"net/url"
"os"
"path/filepath"
"strings"
"unicode"
)
// FromPath creates a URI from OS-specific path per RFC 8089 "file" URI Scheme
func FromPath(rawPath string) string {
// Cleaning up path trims any trailing separator
// which then (in the context of URI below) complies
// with RFC 3986 § 6.2.4 which is relevant in LSP.
path := filepath.Clean(rawPath)
// Convert any OS-specific separators to '/'
path = filepath.ToSlash(path)
volume := filepath.VolumeName(rawPath)
if isWindowsDriveVolume(volume) {
// VSCode normalizes drive letters for unknown reasons.
// See https://github.com/microsoft/vscode/issues/42159#issuecomment-360533151
// While it is a relatively safe assumption that letters are
// case insensitive, this doesn't seem to be documented anywhere.
//
// We just account for VSCode's past decisions here.
path = strings.ToUpper(string(path[0])) + path[1:]
// Per RFC 8089 (Appendix F. Collected Nonstandard Rules)
// file-absolute = "/" drive-letter path-absolute
// i.e. paths with drive-letters (such as C:) are prepended
// with an additional slash.
path = "/" + path
}
u := &url.URL{
Scheme: "file",
Path: path,
}
// Ensure that String() returns uniform escaped path at all times
escapedPath := u.EscapedPath()
if escapedPath != path {
u.RawPath = escapedPath
}
return u.String()
}
// isWindowsDriveVolume returns true if the volume name has a drive letter.
// For example: C:\example.
func isWindowsDriveVolume(path string) bool {
if len(path) < 2 {
return false
}
return unicode.IsLetter(rune(path[0])) && path[1] == ':'
}
// IsURIValid checks whether uri is a valid URI per RFC 8089
func IsURIValid(uri string) bool {
_, err := parseUri(uri)
return err == nil
}
// PathFromURI extracts OS-specific path from an RFC 8089 "file" URI Scheme
func PathFromURI(rawUri string) (string, error) {
uri, err := parseUri(rawUri)
if err != nil {
return "", err
}
// Convert '/' to any OS-specific separators
osPath := filepath.FromSlash(uri.Path)
// Upstream net/url parser prefers consistency and reusability
// (e.g. in HTTP servers) which complies with
// the Comparison Ladder as defined in § 6.2 of RFC 3968.
// https://datatracker.ietf.org/doc/html/rfc3986#section-6.2
//
// Cleaning up path trims any trailing separator
// which then still complies with RFC 3986 per § 6.2.4
// which is relevant in LSP.
osPath = filepath.Clean(osPath)
trimmedOsPath := trimLeftPathSeparator(osPath)
if strings.HasSuffix(filepath.VolumeName(trimmedOsPath), ":") {
// Per RFC 8089 (Appendix F. Collected Nonstandard Rules)
// file-absolute = "/" drive-letter path-absolute
// i.e. paths with drive-letters (such as C:) are preprended
// with an additional slash (which we converted to OS separator above)
// which we trim here.
// See also https://github.com/golang/go/issues/6027
osPath = trimmedOsPath
}
return osPath, nil
}
// MustParseURI returns a normalized RFC 8089 URI.
// It will panic if rawUri is invalid.
//
// Use IsURIValid for checking validity upfront.
func MustParseURI(rawUri string) string {
uri, err := parseUri(rawUri)
if err != nil {
panic(fmt.Sprintf("invalid URI: %s", rawUri))
}
return uri.String()
}
func trimLeftPathSeparator(s string) string {
return strings.TrimLeftFunc(s, func(r rune) bool {
return r == os.PathSeparator
})
}
func MustPathFromURI(uri string) string {
osPath, err := PathFromURI(uri)
if err != nil {
panic(fmt.Sprintf("invalid URI: %s", uri))
}
return osPath
}
func parseUri(rawUri string) (*url.URL, error) {
uri, err := url.ParseRequestURI(rawUri)
if err != nil {
return nil, err
}
if uri.Scheme != "file" {
return nil, fmt.Errorf("unexpected scheme %q in URI %q",
uri.Scheme, rawUri)
}
// Upstream net/url parser prefers consistency and reusability
// (e.g. in HTTP servers) which complies with
// the Comparison Ladder as defined in § 6.2 of RFC 3968.
// https://datatracker.ietf.org/doc/html/rfc3986#section-6.2
// Here we essentially just implement § 6.2.4
// as it is relevant in LSP (which uses the file scheme).
uri.Path = strings.TrimSuffix(uri.Path, "/")
// Upstream net/url parser (correctly) escapes only
// non-ASCII characters as per § 2.1 of RFC 3986.
// https://datatracker.ietf.org/doc/html/rfc3986#section-2.1
// Unfortunately VSCode effectively violates that section
// by escaping ASCII characters such as colon.
// See https://github.com/microsoft/vscode/issues/75027
//
// To account for this we reset RawPath which would
// otherwise be used by String() to effectively enforce
// clean re-escaping of the (unescaped) Path.
uri.RawPath = ""
// The upstream net/url parser (correctly) does not interpret Path
// within URI based on the filesystem or OS where they may (or may not)
// be pointing.
// VSCode normalizes drive letters for unknown reasons.
// See https://github.com/microsoft/vscode/issues/42159#issuecomment-360533151
// While it is a relatively safe assumption that letters are
// case-insensitive, this doesn't seem to be documented anywhere.
//
// We just account for VSCode's past decisions here.
if isLikelyWindowsDriveURIPath(uri.Path) {
uri.Path = string(uri.Path[0]) + strings.ToUpper(string(uri.Path[1])) + uri.Path[2:]
}
return uri, nil
}
// isLikelyWindowsDrivePath returns true if the URI path is of the form used by
// Windows URIs. We check if the URI path has a drive prefix (e.g. "/C:")
func isLikelyWindowsDriveURIPath(uriPath string) bool {
if len(uriPath) < 4 {
return false
}
return uriPath[0] == '/' && unicode.IsLetter(rune(uriPath[1])) && uriPath[2] == ':'
}
// IsWSLURI checks whether URI represents a WSL (Windows Subsystem for Linux)
// UNC path on Windows, such as \\wsl$\Ubuntu\path.
//
// Such a URI represents a common user error since the LS is generally
// expected to run in the same environment where files are located
// (i.e. within the Linux subsystem with Linux paths such as /Ubuntu/path).
func IsWSLURI(uri string) bool {
unescapedPath, err := url.PathUnescape(uri)
if err != nil {
return false
}
u, err := url.ParseRequestURI(unescapedPath)
if err != nil {
return false
}
return u.Scheme == "file" && u.Host == "wsl$"
}
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package main
import (
_ "embed"
"log"
"strings"
"github.com/crossplane-contrib/function-hcl/function-hcl-ls/internal/cmd"
goversion "github.com/hashicorp/go-version"
"github.com/spf13/cobra"
)
var (
// The next version number that will be released. This will be updated after every release
// Version must conform to the format expected by github.com/hashicorp/go-version
// for tests to work.
// A pre-release marker for the version can also be specified (e.g -dev). If this is omitted
// then it means that it is a final release. Otherwise, this is a pre-release
// such as "dev" (in development), "beta", "rc1", etc.
//go:embed version/VERSION
rawVersion string
version = goversion.Must(goversion.NewVersion(strings.TrimSpace(rawVersion)))
)
// VersionString returns the complete version string, including prerelease
func VersionString() string {
return version.String()
}
func main() {
root := &cobra.Command{
Use: "function-hcl-ls",
Short: "Language server for function-hcl",
}
cmd.Version = VersionString()
cmd.AddServeCommand(root)
cmd.AddVersionCommand(root)
cmd.AddDownloadCRDsCommand(root)
cmd.AddDumpASTCommand(root)
if err := root.Execute(); err != nil {
log.Fatalln(err)
}
}