Skip to content
This repository has been archived by the owner on May 29, 2024. It is now read-only.

Commit

Permalink
Support diagnostics using precompile and build
Browse files Browse the repository at this point in the history
  • Loading branch information
harry-hov committed Sep 22, 2023
1 parent 720aaf1 commit e6904ab
Show file tree
Hide file tree
Showing 7 changed files with 334 additions and 1 deletion.
138 changes: 138 additions & 0 deletions internal/lsp/build.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package lsp

import (
"fmt"
"log/slog"
"path/filepath"
"regexp"
"strconv"
"strings"

"github.com/harry-hov/gnopls/internal/tools"
)

type ErrorInfo struct {
FileName string
Line int
Column int
Span []int
Msg string
Tool string
}

func (s *server) PrecompileAndBuild(file *GnoFile) ([]ErrorInfo, error) {
pkgDir := filepath.Dir(file.URI.Filename())
pkgName := filepath.Base(pkgDir)
tmpDir := filepath.Join(s.env.GNOHOME, "gnopls", "tmp", pkgName)

err := copyDir(pkgDir, tmpDir)
if err != nil {
return nil, err
}

preOut, _ := tools.Precompile(tmpDir)
if len(preOut) > 0 {
return parseErrors(file, string(preOut), "precompile")
}

buildOut, _ := tools.Build(tmpDir)
slog.Info(string(buildOut))
return parseErrors(file, string(buildOut), "build")
}

// This is used to extract information from the `gno build` command
// (see `parseError` below).
//
// TODO: Maybe there's a way to get this in a structured format?
var errorRe = regexp.MustCompile(`(?m)^([^#]+?):(\d+):(\d+):(.+)$`)

// parseErrors parses the output of the `gno build` command for errors.
//
// They look something like this:
//
// ```
// command-line-arguments
// # command-line-arguments
// <file>:20:9: undefined: strin
//
// <pkg_path>: build pkg: std go compiler: exit status 1
//
// 1 go build errors
// ```
func parseErrors(file *GnoFile, output, cmd string) ([]ErrorInfo, error) {
errors := []ErrorInfo{}

matches := errorRe.FindAllStringSubmatch(output, -1)
if len(matches) == 0 {
return errors, nil
}

for _, match := range matches {
line, err := strconv.Atoi(match[2])
if err != nil {
return nil, err
}

column, err := strconv.Atoi(match[3])
if err != nil {
return nil, err
}
slog.Info("parsing", "line", line, "column", column, "msg", match[4])

errorInfo := findError(file, match[1], line, column, match[4], cmd)
errors = append(errors, errorInfo)
}

return errors, nil
}

// findError finds the error in the document, shifting the line and column
// numbers to account for the header information in the generated Go file.
func findError(file *GnoFile, fname string, line, col int, msg string, tool string) ErrorInfo {
msg = strings.TrimSpace(msg)

// Error messages are of the form:
//
// <token> <error> (<info>)
// <error>: <token>
//
// We want to strip the parens and find the token in the file.
parens := regexp.MustCompile(`\((.+)\)`)
needle := parens.ReplaceAllString(msg, "")
tokens := strings.Fields(needle)

// The generated Go file has 4 lines of header information.
//
// +1 for zero-indexing.
shiftedLine := line - 4

errorInfo := ErrorInfo{
FileName: strings.TrimPrefix(GoToGnoFileName(filepath.Base(fname)), "."),
Line: shiftedLine,
Column: col,
Span: []int{0, 0},
Msg: msg,
Tool: tool,
}

lines := strings.SplitAfter(string(file.Src), "\n")
for i, l := range lines {
if i != shiftedLine-1 { // zero-indexed
continue
}
for _, token := range tokens {
tokRe := regexp.MustCompile(fmt.Sprintf(`\b%s\b`, regexp.QuoteMeta(token)))
if tokRe.MatchString(l) {
errorInfo.Line = i + 1
errorInfo.Span = []int{col, col + len(token)}
return errorInfo
}
}
}

// If we couldn't find the token, just return the original error + the
// full line.
errorInfo.Span = []int{col, col + 1}

return errorInfo
}
66 changes: 66 additions & 0 deletions internal/lsp/diagnostics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package lsp

import (
"context"
"log/slog"
"path/filepath"
"strings"

"go.lsp.dev/jsonrpc2"
"go.lsp.dev/protocol"
)

func (s *server) publishDiagnostics(ctx context.Context, conn jsonrpc2.Conn, file *GnoFile) error {
slog.Info("Lint", "path", file.URI.Filename())

errors, err := s.PrecompileAndBuild(file)
if err != nil {
return err
}

mPublishDiagnosticParams := make(map[string]*protocol.PublishDiagnosticsParams)
publishDiagnosticParams := make([]*protocol.PublishDiagnosticsParams, 0)
for _, er := range errors {
if !strings.HasSuffix(file.URI.Filename(), er.FileName) {
continue
}
diagnostic := protocol.Diagnostic{
Range: *posToRange(er.Line, er.Span),
Severity: protocol.DiagnosticSeverityError,
Source: "gnopls",
Message: er.Msg,
Code: er.Tool,
}
if pdp, ok := mPublishDiagnosticParams[er.FileName]; ok {
pdp.Diagnostics = append(pdp.Diagnostics, diagnostic)
continue
}
publishDiagnosticParam := protocol.PublishDiagnosticsParams{
URI: file.URI,
Diagnostics: []protocol.Diagnostic{diagnostic},
}
publishDiagnosticParams = append(publishDiagnosticParams, &publishDiagnosticParam)
mPublishDiagnosticParams[er.FileName] = &publishDiagnosticParam
}

// Clean old diagnosed errors if no error found for current file
found := false
for _, er := range errors {
if strings.HasSuffix(er.FileName, filepath.Base(file.URI.Filename())) {
found = true
break
}
}
if !found {
publishDiagnosticParams = append(publishDiagnosticParams, &protocol.PublishDiagnosticsParams{
URI: file.URI,
Diagnostics: []protocol.Diagnostic{},
})
}

return conn.Notify(
ctx,
protocol.MethodTextDocumentPublishDiagnostics,
publishDiagnosticParams,
)
}
5 changes: 4 additions & 1 deletion internal/lsp/general.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ func (s *server) DidOpen(ctx context.Context, reply jsonrpc2.Replier, req jsonrp
s.snapshot.file.Set(uri.Filename(), file)

slog.Info("open " + string(params.TextDocument.URI.Filename()))
return reply(ctx, nil, nil)
notification := s.publishDiagnostics(ctx, s.conn, file)
return reply(ctx, notification, nil)
}

func (s *server) DidClose(ctx context.Context, reply jsonrpc2.Replier, req jsonrpc2.Request) error {
Expand Down Expand Up @@ -72,4 +73,6 @@ func (s *server) DidSave(ctx context.Context, reply jsonrpc2.Replier, req jsonrp
}

slog.Info("save " + string(uri.Filename()))
notification := s.publishDiagnostics(ctx, s.conn, file)
return reply(ctx, notification, nil)
}
1 change: 1 addition & 0 deletions internal/lsp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"go.lsp.dev/protocol"

"github.com/harry-hov/gnopls/internal/env"
"github.com/harry-hov/gnopls/internal/tools"
"github.com/harry-hov/gnopls/internal/version"
)

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

import (
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"strings"

"go.lsp.dev/protocol"
)

// GoToGnoFileName return gno file name from generated go file
// If not a generated go file, return unchanged fname
func GoToGnoFileName(fname string) string {
fname = strings.TrimSuffix(fname, ".gen_test.go")
fname = strings.TrimSuffix(fname, ".gen.go")
return fname
}

// copyDir copies the content of src to dst (not the src dir itself),
// the paths have to be absolute to ensure consistent behavior.
func copyDir(src, dst string) error {
if !filepath.IsAbs(src) || !filepath.IsAbs(dst) {
return fmt.Errorf("src or dst path not absolute, src: %s dst: %s", src, dst)
}

entries, err := os.ReadDir(src)
if err != nil {
return fmt.Errorf("cannot read dir: %s", src)
}

if err := os.MkdirAll(dst, 0o755); err != nil {
return fmt.Errorf("failed to create directory: '%s', error: '%w'", dst, err)
}

for _, entry := range entries {
srcPath := filepath.Join(src, entry.Name())
dstPath := filepath.Join(dst, entry.Name())

if entry.Type().IsDir() {
copyDir(srcPath, dstPath)
} else if entry.Type().IsRegular() {
copyFile(srcPath, dstPath)
}
}

return nil
}

// copyFile copies the file from src to dst, the paths have
// to be absolute to ensure consistent behavior.
func copyFile(src, dst string) error {
if !filepath.IsAbs(src) || !filepath.IsAbs(dst) {
return fmt.Errorf("src or dst path not absolute, src: %s dst: %s", src, dst)
}

// verify if it's regular flile
srcStat, err := os.Stat(src)
if err != nil {
return fmt.Errorf("cannot copy file: %w", err)
}
if !srcStat.Mode().IsRegular() {
return fmt.Errorf("%s not a regular file", src)
}

// create dst file
dstFile, err := os.Create(dst)
if err != nil {
return err
}
defer dstFile.Close()

// open src file
srcFile, err := os.Open(src)
if err != nil {
return err
}
defer srcFile.Close()

// copy srcFile -> dstFile
_, err = io.Copy(dstFile, srcFile)
if err != nil {
return err
}

return nil
}

func posToRange(line int, span []int) *protocol.Range {
return &protocol.Range{
Start: protocol.Position{
Line: uint32(line - 1),
Character: uint32(span[0] - 1),
},
End: protocol.Position{
Line: uint32(line - 1),
Character: uint32(span[1] - 1),
},
}
}

11 changes: 11 additions & 0 deletions internal/tools/build.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package tools

import (
"os/exec"
"path/filepath"
)

// Build a Gno package: gno build <dir>.
func Build(rootDir string) ([]byte, error) {
return exec.Command("gno", "build", filepath.Join(rootDir)).CombinedOutput()
}
11 changes: 11 additions & 0 deletions internal/tools/precompile.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package tools

import (
"os/exec"
"path/filepath"
)

// Precompile a Gno package: gno precompile <dir>.
func Precompile(rootDir string) ([]byte, error) {
return exec.Command("gno", "precompile", filepath.Join(rootDir)).CombinedOutput()
}

0 comments on commit e6904ab

Please sign in to comment.