This repository has been archived by the owner on May 29, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Support diagnostics using precompile and build
- Loading branch information
Showing
7 changed files
with
334 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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), | ||
}, | ||
} | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} |