2293 lines
56 KiB
Go
2293 lines
56 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/format"
|
|
"go/parser"
|
|
"go/token"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"regexp"
|
|
"runtime"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"unicode"
|
|
|
|
"gotlin/internal/lang"
|
|
)
|
|
|
|
const (
|
|
textDocumentSyncFull = 1
|
|
diagnosticSeverityError = 1
|
|
diagnosticSeverityWarning = 2
|
|
symbolKindPackage = 4
|
|
symbolKindClass = 5
|
|
symbolKindMethod = 6
|
|
symbolKindField = 8
|
|
symbolKindVariable = 13
|
|
symbolKindFunction = 12
|
|
symbolKindInterface = 11
|
|
symbolKindModule = 2
|
|
)
|
|
|
|
var offsetPattern = regexp.MustCompile(` at (\d+)`)
|
|
|
|
func main() {
|
|
server := server{
|
|
in: bufio.NewReader(os.Stdin),
|
|
out: os.Stdout,
|
|
docs: map[string]documentState{},
|
|
goplsPath: resolveGoplsPath(),
|
|
}
|
|
if err := server.run(); err != nil && !errors.Is(err, io.EOF) {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
type server struct {
|
|
in *bufio.Reader
|
|
out io.Writer
|
|
docs map[string]documentState
|
|
shutdown bool
|
|
goplsPath string
|
|
}
|
|
|
|
type documentState struct {
|
|
text string
|
|
program *lang.Program
|
|
diagnostics []diagnostic
|
|
symbols []symbol
|
|
}
|
|
|
|
type symbol struct {
|
|
Name string
|
|
Kind int
|
|
Detail string
|
|
Range rng
|
|
Targets []string
|
|
}
|
|
|
|
type variableDeclInfo struct {
|
|
Name string
|
|
Mutable bool
|
|
Type string
|
|
}
|
|
|
|
type request struct {
|
|
JSONRPC string `json:"jsonrpc"`
|
|
ID json.RawMessage `json:"id,omitempty"`
|
|
Method string `json:"method"`
|
|
Params json.RawMessage `json:"params,omitempty"`
|
|
}
|
|
|
|
type response struct {
|
|
JSONRPC string `json:"jsonrpc"`
|
|
ID json.RawMessage `json:"id,omitempty"`
|
|
Result any `json:"result"`
|
|
Error *respError `json:"error,omitempty"`
|
|
}
|
|
|
|
type respError struct {
|
|
Code int `json:"code"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
type notification struct {
|
|
JSONRPC string `json:"jsonrpc"`
|
|
Method string `json:"method"`
|
|
Params any `json:"params,omitempty"`
|
|
}
|
|
|
|
type didOpenParams struct {
|
|
TextDocument textDocumentItem `json:"textDocument"`
|
|
}
|
|
|
|
type didChangeParams struct {
|
|
TextDocument versionedTextDocumentIdentifier `json:"textDocument"`
|
|
ContentChanges []contentChange `json:"contentChanges"`
|
|
}
|
|
|
|
type didCloseParams struct {
|
|
TextDocument textDocumentIdentifier `json:"textDocument"`
|
|
}
|
|
|
|
type hoverParams struct {
|
|
TextDocument textDocumentIdentifier `json:"textDocument"`
|
|
Position position `json:"position"`
|
|
}
|
|
|
|
type definitionParams struct {
|
|
TextDocument textDocumentIdentifier `json:"textDocument"`
|
|
Position position `json:"position"`
|
|
}
|
|
|
|
type textDocumentItem struct {
|
|
URI string `json:"uri"`
|
|
Text string `json:"text"`
|
|
}
|
|
|
|
type versionedTextDocumentIdentifier struct {
|
|
URI string `json:"uri"`
|
|
}
|
|
|
|
type textDocumentIdentifier struct {
|
|
URI string `json:"uri"`
|
|
}
|
|
|
|
type contentChange struct {
|
|
Text string `json:"text"`
|
|
}
|
|
|
|
type publishDiagnosticsParams struct {
|
|
URI string `json:"uri"`
|
|
Diagnostics []diagnostic `json:"diagnostics"`
|
|
}
|
|
|
|
type diagnostic struct {
|
|
Range rng `json:"range"`
|
|
Severity int `json:"severity,omitempty"`
|
|
Source string `json:"source,omitempty"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
type rng struct {
|
|
Start position `json:"start"`
|
|
End position `json:"end"`
|
|
}
|
|
|
|
type position struct {
|
|
Line int `json:"line"`
|
|
Character int `json:"character"`
|
|
}
|
|
|
|
type location struct {
|
|
URI string `json:"uri"`
|
|
Range rng `json:"range"`
|
|
}
|
|
|
|
type stdlibTarget struct {
|
|
PackagePath string
|
|
SymbolName string
|
|
}
|
|
|
|
type stdlibSymbol struct {
|
|
FileName string
|
|
Start position
|
|
End position
|
|
Decl string
|
|
Doc string
|
|
}
|
|
|
|
func (s *server) run() error {
|
|
for {
|
|
msg, err := readMessage(s.in)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var req request
|
|
if err := json.Unmarshal(msg, &req); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := s.handle(req); err != nil {
|
|
return err
|
|
}
|
|
if s.shutdown && req.Method == "exit" {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *server) handle(req request) error {
|
|
switch req.Method {
|
|
case "initialize":
|
|
return s.writeResponse(response{
|
|
JSONRPC: "2.0",
|
|
ID: req.ID,
|
|
Result: map[string]any{
|
|
"capabilities": map[string]any{
|
|
"textDocumentSync": textDocumentSyncFull,
|
|
"hoverProvider": true,
|
|
"definitionProvider": true,
|
|
},
|
|
"serverInfo": map[string]any{
|
|
"name": "gotlin-lsp",
|
|
"version": "0.2.0",
|
|
},
|
|
},
|
|
})
|
|
case "initialized":
|
|
return nil
|
|
case "shutdown":
|
|
s.shutdown = true
|
|
return s.writeResponse(response{JSONRPC: "2.0", ID: req.ID, Result: nil})
|
|
case "exit":
|
|
return nil
|
|
case "textDocument/didOpen":
|
|
var params didOpenParams
|
|
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
|
return err
|
|
}
|
|
s.docs[params.TextDocument.URI] = buildDocumentState(params.TextDocument.Text)
|
|
return s.publishDiagnostics(params.TextDocument.URI)
|
|
case "textDocument/didChange":
|
|
var params didChangeParams
|
|
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
|
return err
|
|
}
|
|
text := ""
|
|
if len(params.ContentChanges) > 0 {
|
|
text = params.ContentChanges[len(params.ContentChanges)-1].Text
|
|
}
|
|
s.docs[params.TextDocument.URI] = buildDocumentState(text)
|
|
return s.publishDiagnostics(params.TextDocument.URI)
|
|
case "textDocument/didClose":
|
|
var params didCloseParams
|
|
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
|
return err
|
|
}
|
|
delete(s.docs, params.TextDocument.URI)
|
|
return s.writeNotification(notification{
|
|
JSONRPC: "2.0",
|
|
Method: "textDocument/publishDiagnostics",
|
|
Params: publishDiagnosticsParams{URI: params.TextDocument.URI, Diagnostics: []diagnostic{}},
|
|
})
|
|
case "textDocument/hover":
|
|
var params hoverParams
|
|
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
|
return err
|
|
}
|
|
return s.writeResponse(response{
|
|
JSONRPC: "2.0",
|
|
ID: req.ID,
|
|
Result: s.hover(params.TextDocument.URI, params.Position),
|
|
})
|
|
case "textDocument/definition":
|
|
var params definitionParams
|
|
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
|
return err
|
|
}
|
|
return s.writeResponse(response{
|
|
JSONRPC: "2.0",
|
|
ID: req.ID,
|
|
Result: s.definition(params.TextDocument.URI, params.Position),
|
|
})
|
|
default:
|
|
if len(req.ID) == 0 {
|
|
return nil
|
|
}
|
|
return s.writeResponse(response{
|
|
JSONRPC: "2.0",
|
|
ID: req.ID,
|
|
Error: &respError{Code: -32601, Message: "method not found"},
|
|
})
|
|
}
|
|
}
|
|
|
|
func buildDocumentState(text string) documentState {
|
|
state := documentState{text: text, diagnostics: []diagnostic{}}
|
|
program, err := lang.Parse(text)
|
|
if err != nil {
|
|
state.diagnostics = []diagnostic{diagnosticFromError(text, err)}
|
|
return state
|
|
}
|
|
state.program = program
|
|
state.symbols = indexSymbols(text, program)
|
|
state.diagnostics = semanticDiagnostics(text, program, state.symbols)
|
|
if _, err := lang.GenerateGo(program); err != nil {
|
|
state.diagnostics = append(state.diagnostics, diagnosticFromError(text, err))
|
|
}
|
|
return state
|
|
}
|
|
|
|
func (s *server) publishDiagnostics(uri string) error {
|
|
state, ok := s.docs[uri]
|
|
if !ok {
|
|
return nil
|
|
}
|
|
diagnostics := state.diagnostics
|
|
if diagnostics == nil {
|
|
diagnostics = []diagnostic{}
|
|
}
|
|
return s.writeNotification(notification{
|
|
JSONRPC: "2.0",
|
|
Method: "textDocument/publishDiagnostics",
|
|
Params: publishDiagnosticsParams{
|
|
URI: uri,
|
|
Diagnostics: diagnostics,
|
|
},
|
|
})
|
|
}
|
|
|
|
func (s *server) hover(uri string, pos position) any {
|
|
state, ok := s.docs[uri]
|
|
if !ok {
|
|
return nil
|
|
}
|
|
word, _ := wordAtPosition(state.text, pos)
|
|
if word == "" {
|
|
return nil
|
|
}
|
|
if result := stdlibHover(state, pos); result != nil {
|
|
return result
|
|
}
|
|
for _, sym := range state.symbols {
|
|
if sym.Name != word {
|
|
continue
|
|
}
|
|
if !rangeContains(sym.Range, pos) && !contains(sym.Targets, word) {
|
|
// allow hover on usages by name match
|
|
}
|
|
return map[string]any{
|
|
"contents": map[string]any{
|
|
"kind": "markdown",
|
|
"value": "```gotlin\n" + sym.Detail + "\n```",
|
|
},
|
|
}
|
|
}
|
|
if value, ok := builtinHoverDetail(word); ok {
|
|
return map[string]any{
|
|
"contents": map[string]any{
|
|
"kind": "markdown",
|
|
"value": "```gotlin\n" + value + "\n```",
|
|
},
|
|
}
|
|
}
|
|
if result := s.goplsHover(state, pos); result != nil {
|
|
return result
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *server) definition(uri string, pos position) any {
|
|
state, ok := s.docs[uri]
|
|
if !ok {
|
|
return nil
|
|
}
|
|
word, _ := wordAtPosition(state.text, pos)
|
|
if word == "" {
|
|
return nil
|
|
}
|
|
if result := stdlibDefinition(state, pos); result != nil {
|
|
return result
|
|
}
|
|
for _, sym := range state.symbols {
|
|
if sym.Name == word {
|
|
return []location{{URI: uri, Range: sym.Range}}
|
|
}
|
|
}
|
|
if result := s.goplsDefinition(state, pos); result != nil {
|
|
return result
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *server) goplsHover(state documentState, pos position) any {
|
|
if s.goplsPath == "" || state.program == nil {
|
|
return nil
|
|
}
|
|
query, tokenStart, ok := goplsQueryAtPosition(state.text, pos)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
source, targetOffset, cleanup, err := prepareGoplsSource(state.program, state.text, query, tokenStart)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
defer cleanup()
|
|
|
|
output, err := exec.Command(s.goplsPath, "hover", fmt.Sprintf("%s:#%d", source, targetOffset)).CombinedOutput()
|
|
if err != nil || len(bytes.TrimSpace(output)) == 0 {
|
|
return nil
|
|
}
|
|
return map[string]any{
|
|
"contents": map[string]any{
|
|
"kind": "markdown",
|
|
"value": "```go\n" + strings.TrimSpace(string(output)) + "\n```",
|
|
},
|
|
}
|
|
}
|
|
|
|
func stdlibHover(state documentState, pos position) any {
|
|
target, ok := resolveStdlibTarget(state, pos)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
sym, ok := findStdlibSymbol(target)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
var value strings.Builder
|
|
value.WriteString("```go\n")
|
|
value.WriteString(sym.Decl)
|
|
value.WriteString("\n```")
|
|
if sym.Doc != "" {
|
|
value.WriteString("\n")
|
|
value.WriteString(sym.Doc)
|
|
}
|
|
return map[string]any{
|
|
"contents": map[string]any{
|
|
"kind": "markdown",
|
|
"value": value.String(),
|
|
},
|
|
}
|
|
}
|
|
|
|
func stdlibDefinition(state documentState, pos position) any {
|
|
target, ok := resolveStdlibTarget(state, pos)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
sym, ok := findStdlibSymbol(target)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
return []location{{
|
|
URI: "file://" + filepath.Clean(sym.FileName),
|
|
Range: rng{
|
|
Start: sym.Start,
|
|
End: sym.End,
|
|
},
|
|
}}
|
|
}
|
|
|
|
func (s *server) goplsDefinition(state documentState, pos position) any {
|
|
if s.goplsPath == "" || state.program == nil {
|
|
return nil
|
|
}
|
|
query, tokenStart, ok := goplsQueryAtPosition(state.text, pos)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
source, targetOffset, cleanup, err := prepareGoplsSource(state.program, state.text, query, tokenStart)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
tempDir := filepath.Dir(source)
|
|
defer cleanup()
|
|
|
|
output, err := exec.Command(s.goplsPath, "definition", fmt.Sprintf("%s:#%d", source, targetOffset)).CombinedOutput()
|
|
if err != nil || len(bytes.TrimSpace(output)) == 0 {
|
|
return nil
|
|
}
|
|
loc, ok := parseGoplsDefinition(string(output))
|
|
if !ok {
|
|
return nil
|
|
}
|
|
if isTempGoplsLocation(loc, tempDir) {
|
|
return nil
|
|
}
|
|
return []location{loc}
|
|
}
|
|
|
|
func indexSymbols(text string, program *lang.Program) []symbol {
|
|
var symbols []symbol
|
|
lines := strings.Split(text, "\n")
|
|
|
|
if program.PackagePath != "" {
|
|
if r, ok := findLineMatch(lines, regexp.MustCompile(`^\s*package\s+([A-Za-z_][\w.-]*(?:\.[A-Za-z_][\w.-]*)*)`), 1); ok {
|
|
symbols = append(symbols, symbol{
|
|
Name: lastPackageSegment(program.PackagePath),
|
|
Kind: symbolKindPackage,
|
|
Detail: "package " + program.PackagePath,
|
|
Range: r,
|
|
})
|
|
}
|
|
}
|
|
|
|
importRanges := findAllLineMatches(lines, regexp.MustCompile(`^\s*import\s+((?:[A-Za-z_][\w-]*\.)*[A-Za-z_][\w-]*)(?:\s+((?:[A-Za-z_][\w-]*\.)*[A-Za-z_][\w-]*))?`))
|
|
for i, imp := range program.Imports {
|
|
name := importAlias(imp)
|
|
if i < len(importRanges) {
|
|
symbols = append(symbols, symbol{
|
|
Name: name,
|
|
Kind: symbolKindModule,
|
|
Detail: renderImportDetail(imp),
|
|
Range: importRanges[i],
|
|
})
|
|
}
|
|
}
|
|
|
|
interfaceRanges := findAllLineMatches(lines, regexp.MustCompile(`^\s*interface\s+([A-Za-z_][A-Za-z0-9_]*)`))
|
|
for i, decl := range program.Interfaces {
|
|
r := rng{}
|
|
if i < len(interfaceRanges) {
|
|
r = interfaceRanges[i]
|
|
}
|
|
symbols = append(symbols, symbol{
|
|
Name: decl.Name,
|
|
Kind: symbolKindInterface,
|
|
Detail: renderInterfaceSignature(decl),
|
|
Range: r,
|
|
})
|
|
for _, method := range decl.Methods {
|
|
symbols = append(symbols, symbol{
|
|
Name: method.Name,
|
|
Kind: symbolKindMethod,
|
|
Detail: renderFunctionSignatureFromParts(method.Name, method.Params, method.ReturnType),
|
|
Range: r,
|
|
Targets: []string{decl.Name},
|
|
})
|
|
}
|
|
}
|
|
|
|
classRanges := findAllLineMatches(lines, regexp.MustCompile(`^\s*class\s+([A-Za-z_][A-Za-z0-9_]*)`))
|
|
for i, decl := range program.Classes {
|
|
r := rng{}
|
|
if i < len(classRanges) {
|
|
r = classRanges[i]
|
|
}
|
|
symbols = append(symbols, symbol{
|
|
Name: decl.Name,
|
|
Kind: symbolKindClass,
|
|
Detail: renderClassSignature(decl),
|
|
Range: r,
|
|
})
|
|
for _, field := range decl.Fields {
|
|
symbols = append(symbols, symbol{
|
|
Name: field.Name,
|
|
Kind: symbolKindField,
|
|
Detail: renderFieldSignature(decl.Name, field),
|
|
Range: r,
|
|
Targets: []string{decl.Name},
|
|
})
|
|
}
|
|
for _, method := range decl.Methods {
|
|
symbols = append(symbols, symbol{
|
|
Name: method.Name,
|
|
Kind: symbolKindMethod,
|
|
Detail: renderMethodSignature(decl.Name, method),
|
|
Range: r,
|
|
Targets: []string{decl.Name},
|
|
})
|
|
}
|
|
}
|
|
workerRanges := findAllLineMatches(lines, regexp.MustCompile(`^\s*worker\s+([A-Za-z_][A-Za-z0-9_]*)`))
|
|
for i, decl := range program.Workers {
|
|
r := rng{}
|
|
if i < len(workerRanges) {
|
|
r = workerRanges[i]
|
|
}
|
|
symbols = append(symbols, symbol{
|
|
Name: decl.Name,
|
|
Kind: symbolKindClass,
|
|
Detail: renderWorkerSignature(decl),
|
|
Range: r,
|
|
})
|
|
for _, field := range decl.Fields {
|
|
symbols = append(symbols, symbol{
|
|
Name: field.Name,
|
|
Kind: symbolKindField,
|
|
Detail: renderWorkerFieldSignature(decl.Name, field),
|
|
Range: r,
|
|
Targets: []string{decl.Name},
|
|
})
|
|
}
|
|
for _, method := range decl.Methods {
|
|
symbols = append(symbols, symbol{
|
|
Name: method.Name,
|
|
Kind: symbolKindMethod,
|
|
Detail: renderMethodSignature(decl.Name, method),
|
|
Range: r,
|
|
Targets: []string{decl.Name},
|
|
})
|
|
}
|
|
}
|
|
|
|
funcRanges := findAllLineMatches(lines, regexp.MustCompile(`^\s*fun\s+([A-Za-z_][A-Za-z0-9_]*)`))
|
|
for i, fn := range program.Functions {
|
|
detail := renderFunctionSignature(fn)
|
|
r := rng{}
|
|
if i < len(funcRanges) {
|
|
r = funcRanges[i]
|
|
}
|
|
symbols = append(symbols, symbol{
|
|
Name: fn.Name,
|
|
Kind: symbolKindFunction,
|
|
Detail: detail,
|
|
Range: r,
|
|
})
|
|
}
|
|
|
|
varRanges := findAllLineNamedMatches(lines, regexp.MustCompile(`^\s*(?:val|var)\s+([A-Za-z_][A-Za-z0-9_]*)`), 1)
|
|
varInfos := collectVariableDeclInfos(program)
|
|
varInfoByName := map[string][]variableDeclInfo{}
|
|
for _, info := range varInfos {
|
|
varInfoByName[info.Name] = append(varInfoByName[info.Name], info)
|
|
}
|
|
for i, decl := range varRanges {
|
|
mutable := false
|
|
typ := ""
|
|
if queue := varInfoByName[decl.Name]; len(queue) > 0 {
|
|
match := queue[0]
|
|
varInfoByName[decl.Name] = queue[1:]
|
|
mutable = match.Mutable
|
|
typ = match.Type
|
|
} else if i < len(varInfos) {
|
|
// Fallback for parser/regex mismatches.
|
|
mutable = varInfos[i].Mutable
|
|
typ = varInfos[i].Type
|
|
}
|
|
symbols = append(symbols, symbol{
|
|
Name: decl.Name,
|
|
Kind: symbolKindVariable,
|
|
Detail: renderVariableSignature(decl.Name, mutable, typ),
|
|
Range: decl.Range,
|
|
})
|
|
}
|
|
|
|
return symbols
|
|
}
|
|
|
|
func semanticDiagnostics(text string, program *lang.Program, symbols []symbol) []diagnostic {
|
|
var diagnostics []diagnostic
|
|
|
|
funcSymbols := map[string]symbol{}
|
|
importSymbols := map[string]symbol{}
|
|
typeSymbols := map[string]symbol{}
|
|
for _, sym := range symbols {
|
|
switch sym.Kind {
|
|
case symbolKindFunction:
|
|
if prev, ok := funcSymbols[sym.Name]; ok {
|
|
diagnostics = append(diagnostics, duplicateDiagnostic(sym.Range, "duplicate function "+sym.Name))
|
|
diagnostics = append(diagnostics, duplicateDiagnostic(prev.Range, "duplicate function "+sym.Name))
|
|
} else {
|
|
funcSymbols[sym.Name] = sym
|
|
}
|
|
case symbolKindModule:
|
|
if prev, ok := importSymbols[sym.Name]; ok {
|
|
diagnostics = append(diagnostics, duplicateDiagnostic(sym.Range, "duplicate import alias "+sym.Name))
|
|
diagnostics = append(diagnostics, duplicateDiagnostic(prev.Range, "duplicate import alias "+sym.Name))
|
|
} else {
|
|
importSymbols[sym.Name] = sym
|
|
}
|
|
case symbolKindClass, symbolKindInterface:
|
|
if prev, ok := typeSymbols[sym.Name]; ok {
|
|
diagnostics = append(diagnostics, duplicateDiagnostic(sym.Range, "duplicate type "+sym.Name))
|
|
diagnostics = append(diagnostics, duplicateDiagnostic(prev.Range, "duplicate type "+sym.Name))
|
|
} else {
|
|
typeSymbols[sym.Name] = sym
|
|
}
|
|
}
|
|
}
|
|
|
|
imports := map[string]bool{}
|
|
for _, imp := range program.Imports {
|
|
imports[importAlias(imp)] = true
|
|
}
|
|
functions := map[string]lang.FunctionDecl{}
|
|
for _, fn := range program.Functions {
|
|
functions[fn.Name] = fn
|
|
}
|
|
types := map[string]bool{}
|
|
for _, decl := range program.Interfaces {
|
|
types[decl.Name] = true
|
|
}
|
|
for _, decl := range program.Classes {
|
|
types[decl.Name] = true
|
|
}
|
|
for _, decl := range program.Workers {
|
|
types[decl.Name] = true
|
|
}
|
|
for _, fn := range program.Functions {
|
|
diagnostics = append(diagnostics, functionSemanticDiagnostics(text, fn, functions, imports, types, nil)...)
|
|
}
|
|
for _, class := range program.Classes {
|
|
fields := map[string]bool{
|
|
"this": true,
|
|
}
|
|
for _, field := range class.Fields {
|
|
fields[field.Name] = true
|
|
}
|
|
for _, method := range class.Methods {
|
|
diagnostics = append(diagnostics, functionSemanticDiagnostics(text, method, functions, imports, types, fields)...)
|
|
}
|
|
}
|
|
for _, worker := range program.Workers {
|
|
fields := map[string]bool{
|
|
"this": true,
|
|
}
|
|
for _, field := range worker.Fields {
|
|
fields[field.Name] = true
|
|
}
|
|
for _, method := range worker.Methods {
|
|
diagnostics = append(diagnostics, functionSemanticDiagnostics(text, method, functions, imports, types, fields)...)
|
|
}
|
|
}
|
|
|
|
sort.SliceStable(diagnostics, func(i, j int) bool {
|
|
if diagnostics[i].Range.Start.Line != diagnostics[j].Range.Start.Line {
|
|
return diagnostics[i].Range.Start.Line < diagnostics[j].Range.Start.Line
|
|
}
|
|
return diagnostics[i].Range.Start.Character < diagnostics[j].Range.Start.Character
|
|
})
|
|
return uniqueDiagnostics(diagnostics)
|
|
}
|
|
|
|
func functionSemanticDiagnostics(text string, fn lang.FunctionDecl, functions map[string]lang.FunctionDecl, imports map[string]bool, types map[string]bool, fields map[string]bool) []diagnostic {
|
|
scope := map[string]bool{}
|
|
for _, param := range fn.Params {
|
|
scope[param.Name] = true
|
|
}
|
|
for name := range fields {
|
|
scope[name] = true
|
|
}
|
|
var diagnostics []diagnostic
|
|
var walkExpr func(expr lang.Expr, scope map[string]bool)
|
|
var walkStmts func(stmts []lang.Stmt, scope map[string]bool)
|
|
|
|
walkStmts = func(stmts []lang.Stmt, scope map[string]bool) {
|
|
for _, stmt := range stmts {
|
|
switch s := stmt.(type) {
|
|
case lang.VarDecl:
|
|
walkExpr(s.Value, scope)
|
|
scope[s.Name] = true
|
|
case lang.MultiVarDecl:
|
|
walkExpr(s.Value, scope)
|
|
for _, name := range s.Names {
|
|
scope[name] = true
|
|
}
|
|
case lang.AssignStmt:
|
|
if !scope[s.Name] {
|
|
diagnostics = append(diagnostics, undefinedNameDiagnostic(text, s.Name, "undefined variable "+s.Name))
|
|
}
|
|
walkExpr(s.Value, scope)
|
|
case lang.AddAssignStmt:
|
|
if !scope[s.Name] {
|
|
diagnostics = append(diagnostics, undefinedNameDiagnostic(text, s.Name, "undefined variable "+s.Name))
|
|
}
|
|
walkExpr(s.Value, scope)
|
|
case lang.MultiAssignStmt:
|
|
for _, name := range s.Names {
|
|
if !scope[name] {
|
|
diagnostics = append(diagnostics, undefinedNameDiagnostic(text, name, "undefined variable "+name))
|
|
}
|
|
}
|
|
walkExpr(s.Value, scope)
|
|
case lang.ReturnStmt:
|
|
if s.Value != nil {
|
|
walkExpr(s.Value, scope)
|
|
}
|
|
case lang.ThrowStmt:
|
|
walkExpr(s.Value, scope)
|
|
case lang.GoStmt:
|
|
walkExpr(s.Value, scope)
|
|
case lang.ExprStmt:
|
|
walkExpr(s.Value, scope)
|
|
case lang.IfStmt:
|
|
walkExpr(s.Cond, scope)
|
|
thenScope := copyScope(scope)
|
|
elseScope := copyScope(scope)
|
|
walkStmts(s.Then, thenScope)
|
|
walkStmts(s.Else, elseScope)
|
|
case lang.WhileStmt:
|
|
walkExpr(s.Cond, scope)
|
|
bodyScope := copyScope(scope)
|
|
walkStmts(s.Body, bodyScope)
|
|
case lang.SelectStmt:
|
|
for _, c := range s.Cases {
|
|
walkExpr(c.Source, scope)
|
|
caseScope := copyScope(scope)
|
|
caseScope["it"] = true
|
|
walkStmts(c.Body, caseScope)
|
|
}
|
|
case lang.TryCatchStmt:
|
|
tryScope := copyScope(scope)
|
|
walkStmts(s.TryBody, tryScope)
|
|
catchScope := copyScope(scope)
|
|
catchScope[s.CatchName] = true
|
|
walkStmts(s.CatchBody, catchScope)
|
|
}
|
|
}
|
|
}
|
|
|
|
walkExpr = func(expr lang.Expr, scope map[string]bool) {
|
|
switch e := expr.(type) {
|
|
case lang.IdentExpr:
|
|
if isBuiltin(e.Name) || scope[e.Name] || functions[e.Name].Name != "" || imports[e.Name] || types[e.Name] {
|
|
return
|
|
}
|
|
diagnostics = append(diagnostics, undefinedNameDiagnostic(text, e.Name, "undefined identifier "+e.Name))
|
|
case lang.UnaryExpr:
|
|
walkExpr(e.Value, scope)
|
|
case lang.BinaryExpr:
|
|
walkExpr(e.Left, scope)
|
|
walkExpr(e.Right, scope)
|
|
case lang.CallExpr:
|
|
if ident, ok := e.Callee.(lang.IdentExpr); ok {
|
|
switch ident.Name {
|
|
case "after", "every":
|
|
if len(e.Args) != 1 {
|
|
diagnostics = append(diagnostics, undefinedNameDiagnostic(text, ident.Name, ident.Name+"(ms) expects exactly one Int argument"))
|
|
}
|
|
if len(e.Args) == 1 {
|
|
switch e.Args[0].(type) {
|
|
case lang.StringExpr, lang.BoolExpr, lang.NullExpr:
|
|
diagnostics = append(diagnostics, undefinedNameDiagnostic(text, ident.Name, ident.Name+"(ms) expects an Int argument"))
|
|
}
|
|
}
|
|
if ident.Name == "every" && len(e.Args) == 1 {
|
|
if value, ok := staticIntExprValue(e.Args[0]); ok && value <= 0 {
|
|
diagnostics = append(diagnostics, undefinedNameDiagnostic(text, ident.Name, "every(ms) requires ms > 0"))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
walkExpr(e.Callee, scope)
|
|
for _, arg := range e.Args {
|
|
walkExpr(arg, scope)
|
|
}
|
|
case lang.SelectorExpr:
|
|
walkExpr(e.Receiver, scope)
|
|
case lang.LambdaExpr:
|
|
lambdaScope := copyScope(scope)
|
|
if e.ImplicitIt {
|
|
lambdaScope["it"] = true
|
|
}
|
|
for _, param := range e.Params {
|
|
lambdaScope[param.Name] = true
|
|
}
|
|
walkStmts(e.Body, lambdaScope)
|
|
case lang.NullExpr:
|
|
return
|
|
}
|
|
}
|
|
|
|
walkStmts(fn.Body, scope)
|
|
return diagnostics
|
|
}
|
|
|
|
func diagnosticFromError(text string, err error) diagnostic {
|
|
start := position{}
|
|
end := position{}
|
|
|
|
if match := offsetPattern.FindStringSubmatch(err.Error()); len(match) == 2 {
|
|
if offset, convErr := strconv.Atoi(match[1]); convErr == nil {
|
|
start = offsetToPosition(text, offset)
|
|
end = start
|
|
end.Character++
|
|
}
|
|
}
|
|
|
|
return diagnostic{
|
|
Range: rng{Start: start, End: end},
|
|
Severity: diagnosticSeverityError,
|
|
Source: "gotlin",
|
|
Message: err.Error(),
|
|
}
|
|
}
|
|
|
|
func duplicateDiagnostic(r rng, message string) diagnostic {
|
|
return diagnostic{
|
|
Range: r,
|
|
Severity: diagnosticSeverityWarning,
|
|
Source: "gotlin",
|
|
Message: message,
|
|
}
|
|
}
|
|
|
|
func undefinedNameDiagnostic(text string, name string, message string) diagnostic {
|
|
r := findWordRange(text, name)
|
|
return diagnostic{
|
|
Range: r,
|
|
Severity: diagnosticSeverityWarning,
|
|
Source: "gotlin",
|
|
Message: message,
|
|
}
|
|
}
|
|
|
|
func offsetToPosition(text string, offset int) position {
|
|
runes := []rune(text)
|
|
if offset < 0 {
|
|
offset = 0
|
|
}
|
|
if offset > len(runes) {
|
|
offset = len(runes)
|
|
}
|
|
line := 0
|
|
char := 0
|
|
for i := 0; i < offset; i++ {
|
|
if runes[i] == '\n' {
|
|
line++
|
|
char = 0
|
|
continue
|
|
}
|
|
char++
|
|
}
|
|
return position{Line: line, Character: char}
|
|
}
|
|
|
|
func positionToOffset(text string, pos position) int {
|
|
runes := []rune(text)
|
|
line := 0
|
|
char := 0
|
|
for i, r := range runes {
|
|
if line == pos.Line && char == pos.Character {
|
|
return i
|
|
}
|
|
if r == '\n' {
|
|
line++
|
|
char = 0
|
|
if line > pos.Line {
|
|
return i
|
|
}
|
|
continue
|
|
}
|
|
char++
|
|
}
|
|
return len(runes)
|
|
}
|
|
|
|
func wordAtPosition(text string, pos position) (string, rng) {
|
|
runes := []rune(text)
|
|
offset := positionToOffset(text, pos)
|
|
if len(runes) == 0 {
|
|
return "", rng{}
|
|
}
|
|
if offset >= len(runes) {
|
|
offset = len(runes) - 1
|
|
}
|
|
if !isWordRune(runes[offset]) && offset > 0 && isWordRune(runes[offset-1]) {
|
|
offset--
|
|
}
|
|
if !isWordRune(runes[offset]) {
|
|
return "", rng{}
|
|
}
|
|
start := offset
|
|
for start > 0 && isWordRune(runes[start-1]) {
|
|
start--
|
|
}
|
|
end := offset
|
|
for end+1 < len(runes) && isWordRune(runes[end+1]) {
|
|
end++
|
|
}
|
|
return string(runes[start : end+1]), rng{
|
|
Start: offsetToPosition(text, start),
|
|
End: offsetToPosition(text, end+1),
|
|
}
|
|
}
|
|
|
|
func findWordRange(text string, word string) rng {
|
|
runes := []rune(text)
|
|
target := []rune(word)
|
|
for i := 0; i+len(target) <= len(runes); i++ {
|
|
if string(runes[i:i+len(target)]) != word {
|
|
continue
|
|
}
|
|
beforeOk := i == 0 || !isWordRune(runes[i-1])
|
|
afterOk := i+len(target) == len(runes) || !isWordRune(runes[i+len(target)])
|
|
if beforeOk && afterOk {
|
|
return rng{
|
|
Start: offsetToPosition(text, i),
|
|
End: offsetToPosition(text, i+len(target)),
|
|
}
|
|
}
|
|
}
|
|
return rng{}
|
|
}
|
|
|
|
func isWordRune(r rune) bool {
|
|
return unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_'
|
|
}
|
|
|
|
func readMessage(r *bufio.Reader) ([]byte, error) {
|
|
contentLength := -1
|
|
for {
|
|
line, err := r.ReadString('\n')
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
line = strings.TrimRight(line, "\r\n")
|
|
if line == "" {
|
|
break
|
|
}
|
|
if strings.HasPrefix(strings.ToLower(line), "content-length:") {
|
|
value := strings.TrimSpace(line[len("content-length:"):])
|
|
n, err := strconv.Atoi(value)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
contentLength = n
|
|
}
|
|
}
|
|
if contentLength < 0 {
|
|
return nil, fmt.Errorf("missing Content-Length header")
|
|
}
|
|
body := make([]byte, contentLength)
|
|
if _, err := io.ReadFull(r, body); err != nil {
|
|
return nil, err
|
|
}
|
|
return body, nil
|
|
}
|
|
|
|
func (s *server) writeResponse(resp response) error {
|
|
data, err := json.Marshal(resp)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return writeMessage(s.out, data)
|
|
}
|
|
|
|
func (s *server) writeNotification(note notification) error {
|
|
data, err := json.Marshal(note)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return writeMessage(s.out, data)
|
|
}
|
|
|
|
func writeMessage(w io.Writer, payload []byte) error {
|
|
var buf bytes.Buffer
|
|
fmt.Fprintf(&buf, "Content-Length: %d\r\n\r\n", len(payload))
|
|
buf.Write(payload)
|
|
_, err := w.Write(buf.Bytes())
|
|
return err
|
|
}
|
|
|
|
func findLineMatch(lines []string, pattern *regexp.Regexp, capture int) (rng, bool) {
|
|
matches := findAllLineMatches(lines, pattern)
|
|
if capture == 1 && len(matches) > 0 {
|
|
return matches[0], true
|
|
}
|
|
return rng{}, false
|
|
}
|
|
|
|
func findAllLineMatches(lines []string, pattern *regexp.Regexp) []rng {
|
|
var out []rng
|
|
for i, line := range lines {
|
|
idx := pattern.FindStringSubmatchIndex(line)
|
|
if idx == nil || len(idx) < 4 {
|
|
continue
|
|
}
|
|
start := idx[2]
|
|
end := idx[3]
|
|
out = append(out, rng{
|
|
Start: position{Line: i, Character: utf16Len(line[:start])},
|
|
End: position{Line: i, Character: utf16Len(line[:end])},
|
|
})
|
|
}
|
|
return out
|
|
}
|
|
|
|
type namedRange struct {
|
|
Name string
|
|
Range rng
|
|
}
|
|
|
|
func findAllLineNamedMatches(lines []string, pattern *regexp.Regexp, capture int) []namedRange {
|
|
var out []namedRange
|
|
for i, line := range lines {
|
|
matches := pattern.FindAllStringSubmatchIndex(line, -1)
|
|
for _, idx := range matches {
|
|
cStart := capture * 2
|
|
cEnd := cStart + 1
|
|
if cEnd >= len(idx) || idx[cStart] < 0 || idx[cEnd] < 0 {
|
|
continue
|
|
}
|
|
start := idx[cStart]
|
|
end := idx[cEnd]
|
|
name := line[start:end]
|
|
out = append(out, namedRange{
|
|
Name: name,
|
|
Range: rng{
|
|
Start: position{Line: i, Character: utf16Len(line[:start])},
|
|
End: position{Line: i, Character: utf16Len(line[:end])},
|
|
},
|
|
})
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func utf16Len(s string) int {
|
|
return len([]rune(s))
|
|
}
|
|
|
|
func lastPackageSegment(path string) string {
|
|
parts := strings.Split(path, ".")
|
|
return parts[len(parts)-1]
|
|
}
|
|
|
|
func importAlias(imp lang.ImportDecl) string {
|
|
if imp.Alias != "" {
|
|
return imp.Alias
|
|
}
|
|
path := strings.TrimPrefix(imp.Path, "go.")
|
|
parts := strings.Split(path, ".")
|
|
return parts[len(parts)-1]
|
|
}
|
|
|
|
func renderImportDetail(imp lang.ImportDecl) string {
|
|
if imp.Alias != "" {
|
|
return "import " + imp.Alias + " " + imp.Path
|
|
}
|
|
return "import " + imp.Path
|
|
}
|
|
|
|
func renderFunctionSignature(fn lang.FunctionDecl) string {
|
|
return renderFunctionSignatureFromParts(fn.Name, fn.Params, fn.ReturnType)
|
|
}
|
|
|
|
func renderFunctionSignatureFromParts(name string, fnParams []lang.Param, returnType string) string {
|
|
var params []string
|
|
for _, param := range fnParams {
|
|
params = append(params, param.Name+": "+param.Type)
|
|
}
|
|
signature := "fun " + name + "(" + strings.Join(params, ", ") + ")"
|
|
if returnType != "" && returnType != "Unit" {
|
|
signature += ": " + returnType
|
|
}
|
|
return signature
|
|
}
|
|
|
|
func renderInterfaceSignature(decl lang.InterfaceDecl) string {
|
|
return "interface " + decl.Name
|
|
}
|
|
|
|
func renderClassSignature(decl lang.ClassDecl) string {
|
|
var b strings.Builder
|
|
b.WriteString("class ")
|
|
b.WriteString(decl.Name)
|
|
|
|
var fields []string
|
|
for _, field := range decl.Fields {
|
|
keyword := "val"
|
|
if field.Mutable {
|
|
keyword = "var"
|
|
}
|
|
fields = append(fields, keyword+" "+field.Name+": "+field.Type)
|
|
}
|
|
if len(decl.Fields) > 0 {
|
|
b.WriteString("(")
|
|
b.WriteString(strings.Join(fields, ", "))
|
|
b.WriteString(")")
|
|
}
|
|
if len(decl.Parents) > 0 {
|
|
b.WriteString(": ")
|
|
b.WriteString(strings.Join(decl.Parents, ", "))
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
func renderWorkerSignature(decl lang.WorkerDecl) string {
|
|
return "worker " + decl.Name
|
|
}
|
|
|
|
func renderMethodSignature(className string, fn lang.FunctionDecl) string {
|
|
return className + "." + renderFunctionSignature(fn)
|
|
}
|
|
|
|
func renderFieldSignature(className string, field lang.FieldDecl) string {
|
|
keyword := "val"
|
|
if field.Mutable {
|
|
keyword = "var"
|
|
}
|
|
return className + "." + keyword + " " + field.Name + ": " + field.Type
|
|
}
|
|
|
|
func renderWorkerFieldSignature(workerName string, field lang.WorkerFieldDecl) string {
|
|
keyword := "val"
|
|
if field.Mutable {
|
|
keyword = "var"
|
|
}
|
|
typ := field.Type
|
|
if typ == "" {
|
|
typ = inferWorkerFieldType(field)
|
|
}
|
|
if typ != "" {
|
|
return workerName + "." + keyword + " " + field.Name + ": " + typ
|
|
}
|
|
return workerName + "." + keyword + " " + field.Name
|
|
}
|
|
|
|
func renderVariableSignature(name string, mutable bool, typ string) string {
|
|
keyword := "val"
|
|
if mutable {
|
|
keyword = "var"
|
|
}
|
|
if typ != "" {
|
|
return keyword + " " + name + ": " + typ
|
|
}
|
|
return keyword + " " + name
|
|
}
|
|
|
|
func collectVariableDeclInfos(program *lang.Program) []variableDeclInfo {
|
|
functions := map[string]lang.FunctionDecl{}
|
|
for _, fn := range program.Functions {
|
|
functions[fn.Name] = fn
|
|
}
|
|
classes := map[string]lang.ClassDecl{}
|
|
for _, class := range program.Classes {
|
|
classes[class.Name] = class
|
|
}
|
|
workers := map[string]lang.WorkerDecl{}
|
|
for _, worker := range program.Workers {
|
|
workers[worker.Name] = worker
|
|
}
|
|
|
|
var out []variableDeclInfo
|
|
var walkStmts func(stmts []lang.Stmt, scope map[string]string)
|
|
var inferExprType func(expr lang.Expr, scope map[string]string) string
|
|
var inferCallReturnTypes func(call lang.CallExpr, scope map[string]string) []string
|
|
|
|
inferCallReturnTypes = func(call lang.CallExpr, scope map[string]string) []string {
|
|
switch callee := call.Callee.(type) {
|
|
case lang.IdentExpr:
|
|
if fn, ok := functions[callee.Name]; ok {
|
|
return []string{fn.ReturnType}
|
|
}
|
|
if _, ok := classes[callee.Name]; ok {
|
|
return []string{callee.Name}
|
|
}
|
|
if _, ok := workers[callee.Name]; ok {
|
|
return []string{callee.Name}
|
|
}
|
|
switch callee.Name {
|
|
case "println":
|
|
return []string{"Unit"}
|
|
case "after", "every":
|
|
return []string{"Channel<time.Time>"}
|
|
case "Channel":
|
|
if len(call.TypeArgs) == 1 {
|
|
return []string{"Channel<" + call.TypeArgs[0] + ">"}
|
|
}
|
|
return []string{"Channel<any>"}
|
|
case "listOf", "mutableListOf":
|
|
if len(call.TypeArgs) == 1 {
|
|
return []string{"List<" + call.TypeArgs[0] + ">"}
|
|
}
|
|
if len(call.Args) > 0 {
|
|
elemType := inferExprType(call.Args[0], scope)
|
|
for i := 1; i < len(call.Args); i++ {
|
|
argType := inferExprType(call.Args[i], scope)
|
|
if argType == "" || elemType == "" || argType != elemType {
|
|
elemType = ""
|
|
break
|
|
}
|
|
}
|
|
if elemType != "" {
|
|
return []string{"List<" + elemType + ">"}
|
|
}
|
|
}
|
|
return []string{"List<any>"}
|
|
case "mapOf", "mutableMapOf":
|
|
if len(call.TypeArgs) == 2 {
|
|
return []string{"Map<" + call.TypeArgs[0] + ", " + call.TypeArgs[1] + ">"}
|
|
}
|
|
if len(call.Args) > 0 && len(call.Args)%2 == 0 {
|
|
keyType := inferExprType(call.Args[0], scope)
|
|
valType := inferExprType(call.Args[1], scope)
|
|
for i := 2; i < len(call.Args); i += 2 {
|
|
nextKeyType := inferExprType(call.Args[i], scope)
|
|
nextValType := inferExprType(call.Args[i+1], scope)
|
|
if keyType == "" || nextKeyType == "" || keyType != nextKeyType {
|
|
keyType = ""
|
|
}
|
|
if valType == "" || nextValType == "" || valType != nextValType {
|
|
valType = ""
|
|
}
|
|
}
|
|
if keyType != "" && valType != "" {
|
|
return []string{"Map<" + keyType + ", " + valType + ">"}
|
|
}
|
|
}
|
|
return []string{"Map<any, any>"}
|
|
default:
|
|
return nil
|
|
}
|
|
case lang.SelectorExpr:
|
|
receiverType := inferExprType(callee.Receiver, scope)
|
|
if callee.Name == "read" {
|
|
if elemType, ok := channelElementType(receiverType); ok {
|
|
return []string{elemType}
|
|
}
|
|
return nil
|
|
}
|
|
if callee.Name == "send" {
|
|
return []string{"Unit"}
|
|
}
|
|
switch receiverType {
|
|
case "*sql.DB", "sql.DB":
|
|
switch callee.Name {
|
|
case "Ping":
|
|
return []string{"error"}
|
|
}
|
|
case "*bun.DB", "bun.DB":
|
|
switch callee.Name {
|
|
case "NewSelect":
|
|
return []string{"*bun.SelectQuery"}
|
|
case "NewCreateTable":
|
|
return []string{"*bun.CreateTableQuery"}
|
|
case "NewInsert":
|
|
return []string{"*bun.InsertQuery"}
|
|
}
|
|
case "*bun.SelectQuery", "bun.SelectQuery":
|
|
switch callee.Name {
|
|
case "ColumnExpr", "Model":
|
|
return []string{"*bun.SelectQuery"}
|
|
case "Scan":
|
|
return []string{"error"}
|
|
case "Count":
|
|
return []string{"Int", "error"}
|
|
}
|
|
case "*bun.CreateTableQuery", "bun.CreateTableQuery":
|
|
switch callee.Name {
|
|
case "Model", "IfNotExists":
|
|
return []string{"*bun.CreateTableQuery"}
|
|
case "Exec":
|
|
return []string{"sql.Result", "error"}
|
|
}
|
|
case "*bun.InsertQuery", "bun.InsertQuery":
|
|
switch callee.Name {
|
|
case "Model":
|
|
return []string{"*bun.InsertQuery"}
|
|
case "Exec":
|
|
return []string{"sql.Result", "error"}
|
|
}
|
|
}
|
|
if path, ok := selectorPathLang(call.Callee); ok {
|
|
switch path {
|
|
case "context.Background":
|
|
return []string{"context.Context"}
|
|
case "sql.OpenDB":
|
|
return []string{"*sql.DB"}
|
|
case "bun.NewDB":
|
|
return []string{"*bun.DB"}
|
|
case "pgdriver.NewConnector":
|
|
return []string{"pgdriver.Connector"}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
inferExprType = func(expr lang.Expr, scope map[string]string) string {
|
|
switch e := expr.(type) {
|
|
case lang.IntExpr:
|
|
return "Int"
|
|
case lang.StringExpr:
|
|
return "String"
|
|
case lang.BoolExpr:
|
|
return "Boolean"
|
|
case lang.NullExpr:
|
|
return "null"
|
|
case lang.IdentExpr:
|
|
return scope[e.Name]
|
|
case lang.CallExpr:
|
|
types := inferCallReturnTypes(e, scope)
|
|
if len(types) > 0 {
|
|
return types[0]
|
|
}
|
|
return ""
|
|
case lang.SelectorExpr:
|
|
receiverType := inferExprType(e.Receiver, scope)
|
|
if receiverType == "" {
|
|
return ""
|
|
}
|
|
trimmed := strings.TrimPrefix(receiverType, "*")
|
|
if class, ok := classes[trimmed]; ok {
|
|
for _, field := range class.Fields {
|
|
if field.Name == e.Name {
|
|
return field.Type
|
|
}
|
|
}
|
|
}
|
|
if worker, ok := workers[trimmed]; ok {
|
|
for _, field := range worker.Fields {
|
|
if field.Name == e.Name {
|
|
if field.Type != "" {
|
|
return field.Type
|
|
}
|
|
return inferWorkerFieldType(field)
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
walkStmts = func(stmts []lang.Stmt, scope map[string]string) {
|
|
for _, stmt := range stmts {
|
|
switch s := stmt.(type) {
|
|
case lang.VarDecl:
|
|
typ := s.Type
|
|
if typ == "" {
|
|
typ = inferExprType(s.Value, scope)
|
|
}
|
|
out = append(out, variableDeclInfo{Name: s.Name, Mutable: s.Mutable, Type: typ})
|
|
scope[s.Name] = typ
|
|
case lang.MultiVarDecl:
|
|
types := []string(nil)
|
|
if call, ok := s.Value.(lang.CallExpr); ok {
|
|
types = inferCallReturnTypes(call, scope)
|
|
}
|
|
for i, name := range s.Names {
|
|
typ := ""
|
|
if i < len(types) {
|
|
typ = types[i]
|
|
}
|
|
out = append(out, variableDeclInfo{Name: name, Mutable: s.Mutable, Type: typ})
|
|
scope[name] = typ
|
|
}
|
|
case lang.AddAssignStmt:
|
|
if _, ok := scope[s.Name]; !ok {
|
|
scope[s.Name] = ""
|
|
}
|
|
case lang.IfStmt:
|
|
thenScope := copyTypeScope(scope)
|
|
elseScope := copyTypeScope(scope)
|
|
walkStmts(s.Then, thenScope)
|
|
walkStmts(s.Else, elseScope)
|
|
case lang.WhileStmt:
|
|
bodyScope := copyTypeScope(scope)
|
|
walkStmts(s.Body, bodyScope)
|
|
case lang.SelectStmt:
|
|
for _, c := range s.Cases {
|
|
caseScope := copyTypeScope(scope)
|
|
if elemType, ok := channelElementType(inferExprType(c.Source, scope)); ok {
|
|
caseScope["it"] = elemType
|
|
} else {
|
|
caseScope["it"] = "any"
|
|
}
|
|
walkStmts(c.Body, caseScope)
|
|
}
|
|
case lang.TryCatchStmt:
|
|
tryScope := copyTypeScope(scope)
|
|
walkStmts(s.TryBody, tryScope)
|
|
catchScope := copyTypeScope(scope)
|
|
catchScope[s.CatchName] = s.CatchType
|
|
walkStmts(s.CatchBody, catchScope)
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, fn := range program.Functions {
|
|
scope := map[string]string{}
|
|
for _, param := range fn.Params {
|
|
scope[param.Name] = param.Type
|
|
}
|
|
walkStmts(fn.Body, scope)
|
|
}
|
|
for _, class := range program.Classes {
|
|
fieldScope := map[string]string{}
|
|
for _, field := range class.Fields {
|
|
fieldScope[field.Name] = field.Type
|
|
}
|
|
for _, method := range class.Methods {
|
|
scope := copyTypeScope(fieldScope)
|
|
for _, param := range method.Params {
|
|
scope[param.Name] = param.Type
|
|
}
|
|
walkStmts(method.Body, scope)
|
|
}
|
|
}
|
|
for _, worker := range program.Workers {
|
|
fieldScope := map[string]string{}
|
|
for _, field := range worker.Fields {
|
|
typ := field.Type
|
|
if typ == "" {
|
|
typ = inferWorkerFieldType(field)
|
|
}
|
|
fieldScope[field.Name] = typ
|
|
}
|
|
for _, method := range worker.Methods {
|
|
scope := copyTypeScope(fieldScope)
|
|
for _, param := range method.Params {
|
|
scope[param.Name] = param.Type
|
|
}
|
|
walkStmts(method.Body, scope)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func copyTypeScope(scope map[string]string) map[string]string {
|
|
dup := make(map[string]string, len(scope))
|
|
for k, v := range scope {
|
|
dup[k] = v
|
|
}
|
|
return dup
|
|
}
|
|
|
|
func channelElementType(typ string) (string, bool) {
|
|
typ = strings.TrimSpace(typ)
|
|
if !strings.HasPrefix(typ, "Channel<") || !strings.HasSuffix(typ, ">") {
|
|
return "", false
|
|
}
|
|
inner := strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(typ, "Channel<"), ">"))
|
|
if inner == "" {
|
|
return "", false
|
|
}
|
|
return inner, true
|
|
}
|
|
|
|
func staticIntExprValue(expr lang.Expr) (int, bool) {
|
|
switch e := expr.(type) {
|
|
case lang.IntExpr:
|
|
v, err := strconv.Atoi(e.Value)
|
|
if err != nil {
|
|
return 0, false
|
|
}
|
|
return v, true
|
|
case lang.UnaryExpr:
|
|
if e.Op != "-" {
|
|
return 0, false
|
|
}
|
|
v, ok := staticIntExprValue(e.Value)
|
|
if !ok {
|
|
return 0, false
|
|
}
|
|
return -v, true
|
|
default:
|
|
return 0, false
|
|
}
|
|
}
|
|
|
|
func inferWorkerFieldType(field lang.WorkerFieldDecl) string {
|
|
if field.Type != "" {
|
|
return field.Type
|
|
}
|
|
switch field.Value.(type) {
|
|
case lang.IntExpr:
|
|
return "Int"
|
|
case lang.StringExpr:
|
|
return "String"
|
|
case lang.BoolExpr:
|
|
return "Boolean"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func selectorPathLang(expr lang.Expr) (string, bool) {
|
|
switch e := expr.(type) {
|
|
case lang.IdentExpr:
|
|
return e.Name, true
|
|
case lang.SelectorExpr:
|
|
left, ok := selectorPathLang(e.Receiver)
|
|
if !ok {
|
|
return "", false
|
|
}
|
|
return left + "." + e.Name, true
|
|
default:
|
|
return "", false
|
|
}
|
|
}
|
|
|
|
func resolveStdlibTarget(state documentState, pos position) (stdlibTarget, bool) {
|
|
if state.program == nil {
|
|
return stdlibTarget{}, false
|
|
}
|
|
imports := map[string]string{}
|
|
for _, imp := range state.program.Imports {
|
|
if strings.HasPrefix(imp.Path, `"`) {
|
|
continue
|
|
}
|
|
imports[importAlias(imp)] = importPathToGoPath(imp.Path)
|
|
}
|
|
if len(imports) == 0 {
|
|
return stdlibTarget{}, false
|
|
}
|
|
|
|
query, _, ok := goplsQueryAtPosition(state.text, pos)
|
|
if ok {
|
|
if alias, symbol, found := splitSelectorQuery(query); found {
|
|
if pkgPath, ok := imports[alias]; ok {
|
|
return stdlibTarget{
|
|
PackagePath: pkgPath,
|
|
SymbolName: symbol,
|
|
}, true
|
|
}
|
|
if inferredAlias, ok := receiverImportAlias(state.program, alias); ok {
|
|
if pkgPath, ok := imports[inferredAlias]; ok {
|
|
return stdlibTarget{
|
|
PackagePath: pkgPath,
|
|
SymbolName: symbol,
|
|
}, true
|
|
}
|
|
}
|
|
}
|
|
if pkgPath, ok := imports[query]; ok {
|
|
return stdlibTarget{
|
|
PackagePath: pkgPath,
|
|
}, true
|
|
}
|
|
}
|
|
|
|
word, _ := wordAtPosition(state.text, pos)
|
|
if word == "" {
|
|
return stdlibTarget{}, false
|
|
}
|
|
if pkgPath, ok := imports[word]; ok {
|
|
return stdlibTarget{
|
|
PackagePath: pkgPath,
|
|
}, true
|
|
}
|
|
if _, r := wordAtPosition(state.text, pos); r != (rng{}) {
|
|
if rootAlias, ok := inferSelectorRootAlias(state.text, r); ok {
|
|
if pkgPath, ok := imports[rootAlias]; ok {
|
|
return stdlibTarget{
|
|
PackagePath: pkgPath,
|
|
SymbolName: word,
|
|
}, true
|
|
}
|
|
if inferredAlias, ok := receiverImportAlias(state.program, rootAlias); ok {
|
|
if pkgPath, ok := imports[inferredAlias]; ok {
|
|
return stdlibTarget{
|
|
PackagePath: pkgPath,
|
|
SymbolName: word,
|
|
}, true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return stdlibTarget{}, false
|
|
}
|
|
|
|
func inferSelectorRootAlias(text string, tokenRange rng) (string, bool) {
|
|
runes := []rune(text)
|
|
start := positionToOffset(text, tokenRange.Start)
|
|
if start <= 0 || start > len(runes) {
|
|
return "", false
|
|
}
|
|
if runes[start-1] != '.' {
|
|
return "", false
|
|
}
|
|
|
|
i := start - 2
|
|
parensDepth := 0
|
|
for i >= 0 {
|
|
ch := runes[i]
|
|
switch ch {
|
|
case ')':
|
|
parensDepth++
|
|
i--
|
|
continue
|
|
case '(':
|
|
if parensDepth > 0 {
|
|
parensDepth--
|
|
i--
|
|
continue
|
|
}
|
|
}
|
|
if parensDepth > 0 {
|
|
i--
|
|
continue
|
|
}
|
|
if unicode.IsSpace(ch) {
|
|
i--
|
|
continue
|
|
}
|
|
if isWordRune(ch) {
|
|
end := i + 1
|
|
for i >= 0 && isWordRune(runes[i]) {
|
|
i--
|
|
}
|
|
startWord := i + 1
|
|
ident := string(runes[startWord:end])
|
|
|
|
j := i
|
|
for j >= 0 && unicode.IsSpace(runes[j]) {
|
|
j--
|
|
}
|
|
if j >= 0 && runes[j] == '.' {
|
|
i = j - 1
|
|
continue
|
|
}
|
|
return ident, true
|
|
}
|
|
return "", false
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
func receiverImportAlias(program *lang.Program, receiver string) (string, bool) {
|
|
for _, class := range program.Classes {
|
|
for _, field := range class.Fields {
|
|
if field.Name != receiver {
|
|
continue
|
|
}
|
|
if alias, ok := firstTypeImportAlias(field.Type); ok {
|
|
return alias, true
|
|
}
|
|
}
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
func firstTypeImportAlias(typ string) (string, bool) {
|
|
typ = strings.TrimSpace(typ)
|
|
for strings.HasPrefix(typ, "*") {
|
|
typ = strings.TrimPrefix(typ, "*")
|
|
}
|
|
dot := strings.Index(typ, ".")
|
|
if dot <= 0 {
|
|
return "", false
|
|
}
|
|
return typ[:dot], true
|
|
}
|
|
|
|
func splitSelectorQuery(query string) (string, string, bool) {
|
|
index := strings.Index(query, ".")
|
|
if index <= 0 || index+1 >= len(query) {
|
|
return "", "", false
|
|
}
|
|
return query[:index], query[index+1:], true
|
|
}
|
|
|
|
func findStdlibSymbol(target stdlibTarget) (stdlibSymbol, bool) {
|
|
dir, ok := resolvePackageDir(target.PackagePath)
|
|
if !ok {
|
|
return stdlibSymbol{}, false
|
|
}
|
|
fset := token.NewFileSet()
|
|
pkgs, err := parser.ParseDir(fset, dir, func(info os.FileInfo) bool {
|
|
return !strings.HasSuffix(info.Name(), "_test.go")
|
|
}, parser.ParseComments)
|
|
if err != nil || len(pkgs) == 0 {
|
|
return stdlibSymbol{}, false
|
|
}
|
|
|
|
pkg := firstASTPackage(pkgs)
|
|
if pkg == nil {
|
|
return stdlibSymbol{}, false
|
|
}
|
|
files := packageFiles(pkg)
|
|
if len(files) == 0 {
|
|
return stdlibSymbol{}, false
|
|
}
|
|
|
|
if target.SymbolName == "" {
|
|
file := files[0]
|
|
start := fset.Position(file.Name.Pos())
|
|
end := fset.Position(file.Name.End())
|
|
decl := "package " + pkg.Name
|
|
doc := strings.TrimSpace(commentText(file.Doc))
|
|
return stdlibSymbol{
|
|
FileName: start.Filename,
|
|
Start: tokenPosition(start),
|
|
End: tokenPosition(end),
|
|
Decl: decl,
|
|
Doc: doc,
|
|
}, true
|
|
}
|
|
|
|
for _, file := range files {
|
|
for _, decl := range file.Decls {
|
|
if sym, ok := matchStdlibDecl(fset, decl, target.SymbolName); ok {
|
|
return sym, true
|
|
}
|
|
}
|
|
}
|
|
return stdlibSymbol{}, false
|
|
}
|
|
|
|
func resolvePackageDir(packagePath string) (string, bool) {
|
|
stdlibDir := filepath.Join(resolveGoRoot(), "src", filepath.FromSlash(packagePath))
|
|
if info, err := os.Stat(stdlibDir); err == nil && info.IsDir() {
|
|
return stdlibDir, true
|
|
}
|
|
|
|
if listed := goListPackageDir(packagePath); listed != "" {
|
|
return listed, true
|
|
}
|
|
|
|
if cached := findInModuleCache(packagePath); cached != "" {
|
|
return cached, true
|
|
}
|
|
|
|
return "", false
|
|
}
|
|
|
|
func goListPackageDir(packagePath string) string {
|
|
output, err := exec.Command("go", "list", "-f", "{{.Dir}}", packagePath).Output()
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
dir := strings.TrimSpace(string(output))
|
|
if dir == "" {
|
|
return ""
|
|
}
|
|
if info, err := os.Stat(dir); err == nil && info.IsDir() {
|
|
return dir
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func findInModuleCache(packagePath string) string {
|
|
modCache := strings.TrimSpace(goEnv("GOMODCACHE"))
|
|
if modCache == "" {
|
|
goPath := strings.TrimSpace(goEnv("GOPATH"))
|
|
if goPath != "" {
|
|
parts := filepath.SplitList(goPath)
|
|
if len(parts) > 0 {
|
|
modCache = filepath.Join(parts[0], "pkg", "mod")
|
|
}
|
|
}
|
|
}
|
|
if modCache == "" {
|
|
return ""
|
|
}
|
|
|
|
parts := strings.Split(packagePath, "/")
|
|
for i := len(parts); i >= 1; i-- {
|
|
modulePath := strings.Join(parts[:i], "/")
|
|
subPath := strings.Join(parts[i:], "/")
|
|
pattern := filepath.Join(modCache, escapeModulePath(modulePath)+"@*")
|
|
matches, _ := filepath.Glob(pattern)
|
|
if len(matches) == 0 {
|
|
continue
|
|
}
|
|
sort.Strings(matches)
|
|
for j := len(matches) - 1; j >= 0; j-- {
|
|
candidate := matches[j]
|
|
if subPath != "" {
|
|
candidate = filepath.Join(candidate, filepath.FromSlash(subPath))
|
|
}
|
|
if info, err := os.Stat(candidate); err == nil && info.IsDir() {
|
|
return candidate
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func escapeModulePath(path string) string {
|
|
var b strings.Builder
|
|
for _, r := range path {
|
|
if r >= 'A' && r <= 'Z' {
|
|
b.WriteRune('!')
|
|
b.WriteRune(r + ('a' - 'A'))
|
|
continue
|
|
}
|
|
b.WriteRune(r)
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
func importPathToGoPath(path string) string {
|
|
trimmed := strings.TrimPrefix(path, "go.")
|
|
parts := strings.Split(trimmed, ".")
|
|
if len(parts) >= 3 && isDomainTLD(parts[1]) {
|
|
return parts[0] + "." + parts[1] + "/" + strings.Join(parts[2:], "/")
|
|
}
|
|
return strings.ReplaceAll(trimmed, ".", "/")
|
|
}
|
|
|
|
func isDomainTLD(segment string) bool {
|
|
switch segment {
|
|
case "com", "org", "net", "io", "dev", "app", "ai":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func firstASTPackage(pkgs map[string]*ast.Package) *ast.Package {
|
|
names := make([]string, 0, len(pkgs))
|
|
for name := range pkgs {
|
|
names = append(names, name)
|
|
}
|
|
sort.Strings(names)
|
|
for _, name := range names {
|
|
return pkgs[name]
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func packageFiles(pkg *ast.Package) []*ast.File {
|
|
names := make([]string, 0, len(pkg.Files))
|
|
for name := range pkg.Files {
|
|
names = append(names, name)
|
|
}
|
|
sort.Strings(names)
|
|
files := make([]*ast.File, 0, len(names))
|
|
for _, name := range names {
|
|
files = append(files, pkg.Files[name])
|
|
}
|
|
return files
|
|
}
|
|
|
|
func matchStdlibDecl(fset *token.FileSet, decl ast.Decl, symbolName string) (stdlibSymbol, bool) {
|
|
switch d := decl.(type) {
|
|
case *ast.FuncDecl:
|
|
if d.Name == nil || d.Name.Name != symbolName {
|
|
return stdlibSymbol{}, false
|
|
}
|
|
return newStdlibSymbol(fset, d.Name.Pos(), d.Name.End(), formatFuncDecl(fset, d), commentText(d.Doc)), true
|
|
case *ast.GenDecl:
|
|
for _, spec := range d.Specs {
|
|
switch s := spec.(type) {
|
|
case *ast.TypeSpec:
|
|
if s.Name.Name != symbolName {
|
|
continue
|
|
}
|
|
return newStdlibSymbol(fset, s.Name.Pos(), s.Name.End(), formatGenDecl(fset, d, s), specCommentText(d.Doc, s.Doc, s.Comment)), true
|
|
case *ast.ValueSpec:
|
|
for _, name := range s.Names {
|
|
if name.Name != symbolName {
|
|
continue
|
|
}
|
|
return newStdlibSymbol(fset, name.Pos(), name.End(), formatGenDecl(fset, d, s), specCommentText(d.Doc, s.Doc, s.Comment)), true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return stdlibSymbol{}, false
|
|
}
|
|
|
|
func newStdlibSymbol(fset *token.FileSet, start token.Pos, end token.Pos, decl string, doc string) stdlibSymbol {
|
|
startPos := fset.Position(start)
|
|
endPos := fset.Position(end)
|
|
return stdlibSymbol{
|
|
FileName: startPos.Filename,
|
|
Start: tokenPosition(startPos),
|
|
End: tokenPosition(endPos),
|
|
Decl: strings.TrimSpace(decl),
|
|
Doc: strings.TrimSpace(doc),
|
|
}
|
|
}
|
|
|
|
func formatFuncDecl(fset *token.FileSet, decl *ast.FuncDecl) string {
|
|
copyDecl := *decl
|
|
copyDecl.Body = nil
|
|
return formatNode(fset, ©Decl)
|
|
}
|
|
|
|
func formatGenDecl(fset *token.FileSet, decl *ast.GenDecl, spec ast.Spec) string {
|
|
copyDecl := &ast.GenDecl{
|
|
Tok: decl.Tok,
|
|
Specs: []ast.Spec{
|
|
spec,
|
|
},
|
|
}
|
|
return formatNode(fset, copyDecl)
|
|
}
|
|
|
|
func formatNode(fset *token.FileSet, node any) string {
|
|
var buf bytes.Buffer
|
|
if err := format.Node(&buf, fset, node); err != nil {
|
|
return ""
|
|
}
|
|
return buf.String()
|
|
}
|
|
|
|
func commentText(group *ast.CommentGroup) string {
|
|
if group == nil {
|
|
return ""
|
|
}
|
|
return strings.TrimSpace(group.Text())
|
|
}
|
|
|
|
func specCommentText(groups ...*ast.CommentGroup) string {
|
|
for _, group := range groups {
|
|
if text := commentText(group); text != "" {
|
|
return text
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func tokenPosition(pos token.Position) position {
|
|
return position{
|
|
Line: pos.Line - 1,
|
|
Character: pos.Column - 1,
|
|
}
|
|
}
|
|
|
|
func resolveGoRoot() string {
|
|
if value := strings.TrimSpace(os.Getenv("GOROOT")); value != "" {
|
|
return value
|
|
}
|
|
if value := strings.TrimSpace(goEnv("GOROOT")); value != "" {
|
|
return value
|
|
}
|
|
return runtime.GOROOT()
|
|
}
|
|
|
|
func copyScope(scope map[string]bool) map[string]bool {
|
|
dup := make(map[string]bool, len(scope))
|
|
for k, v := range scope {
|
|
dup[k] = v
|
|
}
|
|
return dup
|
|
}
|
|
|
|
func isBuiltin(name string) bool {
|
|
switch name {
|
|
case "println", "runCatching", "Channel", "after", "every", "listOf", "mutableListOf", "mapOf", "mutableMapOf":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func builtinHoverDetail(name string) (string, bool) {
|
|
switch name {
|
|
case "after":
|
|
return "fun after(ms: Int): Channel<time.Time>", true
|
|
case "every":
|
|
return "fun every(ms: Int): Channel<time.Time>", true
|
|
default:
|
|
return "", false
|
|
}
|
|
}
|
|
|
|
func contains(values []string, needle string) bool {
|
|
for _, value := range values {
|
|
if value == needle {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func rangeContains(r rng, p position) bool {
|
|
if p.Line < r.Start.Line || p.Line > r.End.Line {
|
|
return false
|
|
}
|
|
if p.Line == r.Start.Line && p.Character < r.Start.Character {
|
|
return false
|
|
}
|
|
if p.Line == r.End.Line && p.Character > r.End.Character {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func uniqueDiagnostics(input []diagnostic) []diagnostic {
|
|
seen := map[string]bool{}
|
|
var out []diagnostic
|
|
for _, d := range input {
|
|
key := fmt.Sprintf("%d:%d:%d:%d:%s", d.Range.Start.Line, d.Range.Start.Character, d.Range.End.Line, d.Range.End.Character, d.Message)
|
|
if seen[key] {
|
|
continue
|
|
}
|
|
seen[key] = true
|
|
out = append(out, d)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func resolveGoplsPath() string {
|
|
if configured := strings.TrimSpace(os.Getenv("GOTLIN_GOPLS_PATH")); configured != "" {
|
|
return configured
|
|
}
|
|
if path, err := exec.LookPath("gopls"); err == nil {
|
|
return path
|
|
}
|
|
for _, dir := range goBinCandidates() {
|
|
path := filepath.Join(dir, "gopls")
|
|
if info, err := os.Stat(path); err == nil && !info.IsDir() {
|
|
return path
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func goBinCandidates() []string {
|
|
var dirs []string
|
|
seen := map[string]bool{}
|
|
add := func(dir string) {
|
|
dir = strings.TrimSpace(dir)
|
|
if dir == "" || seen[dir] {
|
|
return
|
|
}
|
|
seen[dir] = true
|
|
dirs = append(dirs, dir)
|
|
}
|
|
|
|
add(os.Getenv("GOBIN"))
|
|
for _, part := range filepath.SplitList(os.Getenv("GOPATH")) {
|
|
add(filepath.Join(part, "bin"))
|
|
}
|
|
if home, err := os.UserHomeDir(); err == nil {
|
|
add(filepath.Join(home, "go", "bin"))
|
|
}
|
|
for _, value := range []string{goEnv("GOBIN"), goEnv("GOPATH")} {
|
|
for _, part := range filepath.SplitList(value) {
|
|
if filepath.Base(part) == "bin" {
|
|
add(part)
|
|
continue
|
|
}
|
|
add(filepath.Join(part, "bin"))
|
|
}
|
|
}
|
|
return dirs
|
|
}
|
|
|
|
func goEnv(name string) string {
|
|
output, err := exec.Command("go", "env", name).Output()
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return strings.TrimSpace(string(output))
|
|
}
|
|
|
|
func goplsQueryAtPosition(text string, pos position) (string, int, bool) {
|
|
word, r := wordAtPosition(text, pos)
|
|
if word == "" {
|
|
return "", 0, false
|
|
}
|
|
startOffset := positionToOffset(text, r.Start)
|
|
runes := []rune(text)
|
|
|
|
if startOffset > 0 && runes[startOffset-1] == '.' {
|
|
leftEnd := startOffset - 1
|
|
leftStart := leftEnd - 1
|
|
for leftStart >= 0 && isWordRune(runes[leftStart]) {
|
|
leftStart--
|
|
}
|
|
leftStart++
|
|
if leftStart < leftEnd {
|
|
receiver := string(runes[leftStart:leftEnd])
|
|
return receiver + "." + word, startOffset, true
|
|
}
|
|
}
|
|
|
|
return word, startOffset, true
|
|
}
|
|
|
|
func prepareGoplsSource(program *lang.Program, gotlinSource string, query string, tokenStart int) (string, int, func(), error) {
|
|
goSrc, err := lang.GenerateGo(program)
|
|
if err != nil {
|
|
return "", 0, func() {}, err
|
|
}
|
|
|
|
preferredLine := offsetToPosition(gotlinSource, tokenStart).Line
|
|
targetOffset := findQueryOffset(string(goSrc), query, preferredLine)
|
|
if targetOffset < 0 {
|
|
targetOffset = findQueryOffset(string(goSrc), lastQuerySegment(query), preferredLine)
|
|
}
|
|
if targetOffset < 0 {
|
|
return "", 0, func() {}, fmt.Errorf("query %q not found in generated Go", query)
|
|
}
|
|
|
|
dir, err := os.MkdirTemp("", "gotlin-gopls-*")
|
|
if err != nil {
|
|
return "", 0, func() {}, err
|
|
}
|
|
cleanup := func() { _ = os.RemoveAll(dir) }
|
|
|
|
source := filepath.Join(dir, "main.go")
|
|
if err := os.WriteFile(source, goSrc, 0o644); err != nil {
|
|
cleanup()
|
|
return "", 0, func() {}, err
|
|
}
|
|
if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module gotlin-gopls-temp\n\ngo 1.25\n"), 0o644); err != nil {
|
|
cleanup()
|
|
return "", 0, func() {}, err
|
|
}
|
|
|
|
return source, targetOffset, cleanup, nil
|
|
}
|
|
|
|
func findQueryOffset(source string, query string, preferredLine int) int {
|
|
if query == "" {
|
|
return -1
|
|
}
|
|
indices := allQueryOffsets(source, query)
|
|
if len(indices) == 0 {
|
|
return -1
|
|
}
|
|
bestIndex := indices[0]
|
|
bestDistance := absInt(byteLineNumber(source, bestIndex) - preferredLine)
|
|
for _, index := range indices[1:] {
|
|
distance := absInt(byteLineNumber(source, index) - preferredLine)
|
|
if distance < bestDistance {
|
|
bestIndex = index
|
|
bestDistance = distance
|
|
}
|
|
}
|
|
return bestIndex
|
|
}
|
|
|
|
func lastQuerySegment(query string) string {
|
|
if idx := strings.LastIndex(query, "."); idx >= 0 && idx+1 < len(query) {
|
|
return query[idx+1:]
|
|
}
|
|
return query
|
|
}
|
|
|
|
func parseGoplsDefinition(output string) (location, bool) {
|
|
line := strings.TrimSpace(output)
|
|
if line == "" {
|
|
return location{}, false
|
|
}
|
|
first := strings.Split(line, "\n")[0]
|
|
re := regexp.MustCompile(`^(.*):(\d+):(\d+)-(\d+):`)
|
|
match := re.FindStringSubmatch(first)
|
|
if len(match) != 5 {
|
|
return location{}, false
|
|
}
|
|
lineNo, err1 := strconv.Atoi(match[2])
|
|
startCol, err2 := strconv.Atoi(match[3])
|
|
endCol, err3 := strconv.Atoi(match[4])
|
|
if err1 != nil || err2 != nil || err3 != nil {
|
|
return location{}, false
|
|
}
|
|
path := filepath.Clean(match[1])
|
|
return location{
|
|
URI: "file://" + path,
|
|
Range: rng{
|
|
Start: position{Line: lineNo - 1, Character: startCol - 1},
|
|
End: position{Line: lineNo - 1, Character: endCol - 1},
|
|
},
|
|
}, true
|
|
}
|
|
|
|
func isTempGoplsLocation(loc location, tempDir string) bool {
|
|
path := strings.TrimPrefix(loc.URI, "file://")
|
|
cleanPath := filepath.Clean(path)
|
|
cleanTempDir := filepath.Clean(tempDir)
|
|
return cleanPath == cleanTempDir || strings.HasPrefix(cleanPath, cleanTempDir+string(os.PathSeparator))
|
|
}
|
|
|
|
func allQueryOffsets(source string, query string) []int {
|
|
var offsets []int
|
|
for start := 0; start < len(source); {
|
|
index := strings.Index(source[start:], query)
|
|
if index < 0 {
|
|
break
|
|
}
|
|
offset := start + index
|
|
offsets = append(offsets, offset)
|
|
start = offset + len(query)
|
|
}
|
|
return offsets
|
|
}
|
|
|
|
func byteLineNumber(source string, offset int) int {
|
|
if offset < 0 {
|
|
offset = 0
|
|
}
|
|
if offset > len(source) {
|
|
offset = len(source)
|
|
}
|
|
line := 0
|
|
for i := 0; i < offset; i++ {
|
|
if source[i] == '\n' {
|
|
line++
|
|
}
|
|
}
|
|
return line
|
|
}
|
|
|
|
func absInt(n int) int {
|
|
if n < 0 {
|
|
return -n
|
|
}
|
|
return n
|
|
}
|