Skip to content

Commit

Permalink
feat_: LogOnPanic linter
Browse files Browse the repository at this point in the history
  • Loading branch information
igor-sirotin committed Oct 19, 2024
1 parent 9f56401 commit 0ac1275
Show file tree
Hide file tree
Showing 2,410 changed files with 26,186 additions and 113,508 deletions.
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,10 @@ canary-test: node-canary
# TODO: uncomment that!
#_assets/scripts/canary_test_mailservers.sh ./config/cli/fleet-eth.prod.json

lint: generate
lint-panics: generate
go run ./cmd/lint-panics --test=false ./...

lint: generate lint-panics
golangci-lint run ./...

ci: generate lint canary-test test-unit test-e2e ##@tests Run all linters and tests at once
Expand Down
101 changes: 101 additions & 0 deletions cmd/lint-panics/gopls/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package gopls

import (
"context"
"go.lsp.dev/protocol"

"go.uber.org/zap"
)

type DummyClient struct {
logger *zap.Logger
}

func NewDummyClient(logger *zap.Logger) *DummyClient {
return &DummyClient{
logger: nil, //logger.Named("client"),
}
}

func (d *DummyClient) Progress(ctx context.Context, params *protocol.ProgressParams) (err error) {
if d.logger != nil {
d.logger.Debug("client: Progress", zap.Any("params", params))
}
return
}
func (d *DummyClient) WorkDoneProgressCreate(ctx context.Context, params *protocol.WorkDoneProgressCreateParams) (err error) {
if d.logger != nil {
d.logger.Debug("client: WorkDoneProgressCreate")
}
return nil
}

func (d *DummyClient) LogMessage(ctx context.Context, params *protocol.LogMessageParams) (err error) {
if d.logger != nil {
d.logger.Debug("client: LogMessage", zap.Any("message", params))
}
return nil
}

func (d *DummyClient) PublishDiagnostics(ctx context.Context, params *protocol.PublishDiagnosticsParams) (err error) {
if d.logger != nil {
d.logger.Debug("client: PublishDiagnostics")
}
return nil
}

func (d *DummyClient) ShowMessage(ctx context.Context, params *protocol.ShowMessageParams) (err error) {
if d.logger != nil {
d.logger.Debug("client: ShowMessage", zap.Any("message", params))
}
return nil
}

func (d *DummyClient) ShowMessageRequest(ctx context.Context, params *protocol.ShowMessageRequestParams) (result *protocol.MessageActionItem, err error) {
if d.logger != nil {
d.logger.Debug("client: ShowMessageRequest", zap.Any("message", params))
}
return nil, nil
}

func (d *DummyClient) Telemetry(ctx context.Context, params interface{}) (err error) {
if d.logger != nil {
d.logger.Debug("client: Telemetry")
}
return nil
}

func (d *DummyClient) RegisterCapability(ctx context.Context, params *protocol.RegistrationParams) (err error) {
if d.logger != nil {
d.logger.Debug("client: RegisterCapability")
}
return nil
}

func (d *DummyClient) UnregisterCapability(ctx context.Context, params *protocol.UnregistrationParams) (err error) {
if d.logger != nil {
d.logger.Debug("client: UnregisterCapability")
}
return nil
}

func (d *DummyClient) ApplyEdit(ctx context.Context, params *protocol.ApplyWorkspaceEditParams) (result bool, err error) {
if d.logger != nil {
d.logger.Debug("client: ApplyEdit")
}
return false, nil
}

func (d *DummyClient) Configuration(ctx context.Context, params *protocol.ConfigurationParams) (result []interface{}, err error) {
if d.logger != nil {
d.logger.Debug("client: Configuration")
}
return nil, nil
}

func (d *DummyClient) WorkspaceFolders(ctx context.Context) (result []protocol.WorkspaceFolder, err error) {
if d.logger != nil {
d.logger.Debug("client: WorkspaceFolders")
}
return nil, nil
}
150 changes: 150 additions & 0 deletions cmd/lint-panics/gopls/gopls.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package gopls

import (
"os/exec"
"github.com/pkg/errors"

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

"time"
"go.uber.org/zap"
"go.lsp.dev/uri"
)

type Connection struct {
logger *zap.Logger
server protocol.Server
cmd *exec.Cmd
conn jsonrpc2.Conn
}

func NewGoplsClient(ctx context.Context, logger *zap.Logger, rootDir string) *Connection {
var err error

logger.Debug("initializing gopls client")

gopls := &Connection{
logger: logger,
}

client := NewDummyClient(logger)

// Create a JSON-RPC connection using stdin and stdout
gopls.cmd = exec.Command("gopls", "serve")

stdin, err := gopls.cmd.StdinPipe()
if err != nil {
logger.Error("Failed to get stdin pipe", zap.Error(err))
panic(err)
}

stdout, err := gopls.cmd.StdoutPipe()
if err != nil {
logger.Error("Failed to get stdout pipe", zap.Error(err))
panic(err)
}

err = gopls.cmd.Start()
if err != nil {
logger.Error("Failed to start gopls", zap.Error(err))
panic(err)
}

stream := jsonrpc2.NewStream(&CombinedReadWriteCloser{
stdin: stdin,
stdout: stdout,
})

ctx, gopls.conn, gopls.server = protocol.NewClient(ctx, client, stream, logger)

initParams := protocol.InitializeParams{
RootURI: uri.From("file", "", rootDir, "", ""),
InitializationOptions: map[string]interface{}{
"symbolMatcher": "FastFuzzy",
},
}

_, err = gopls.server.Initialize(ctx, &initParams)
if err != nil {
logger.Error("Error during initialize", zap.Error(err))
panic(err)
}

// Step 2: Send 'initialized' notification
err = gopls.server.Initialized(ctx, &protocol.InitializedParams{})
if err != nil {
logger.Error("Error during initialized", zap.Error(err))
panic(err)
}

return gopls
}

func (gopls *Connection) Definition(ctx context.Context, filePath string, lineNumber int, charPosition int) (string, int, error) {
// NOTE: gopls uses 0-based line and column numbers
defFile, defLine, err := gopls.definition(ctx, filePath, lineNumber-1, charPosition-1)
return defFile, defLine + 1, err
}

func (gopls *Connection) definition(ctx context.Context, filePath string, lineNumber int, charPosition int) (string, int, error) {
// Define the file URI and position where the function/method is invoked
fileURI := protocol.DocumentURI("file://" + filePath) // Replace with actual file URI
line := lineNumber // Line number where the function is called
character := charPosition // Character (column) where the function is called

// Send the definition request
params := &protocol.DefinitionParams{
TextDocumentPositionParams: protocol.TextDocumentPositionParams{
TextDocument: protocol.TextDocumentIdentifier{
URI: fileURI,
},
Position: protocol.Position{
Line: uint32(line),
Character: uint32(character),
},
},
}

// Create context with a timeout to avoid hanging
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()

locations, err := gopls.server.Definition(ctx, params)
if err != nil {
return "", 0, errors.Wrap(err, "failed to fetch definition")
}

if len(locations) == 0 {
return "", 0, errors.New("no definition found")
}

location := locations[0]
return location.URI.Filename(), int(location.Range.Start.Line), nil
}

func (gopls *Connection) DidOpen(ctx context.Context, path string, content string, logger *zap.Logger) {
err := gopls.server.DidOpen(ctx, &protocol.DidOpenTextDocumentParams{
TextDocument: protocol.TextDocumentItem{
URI: protocol.DocumentURI(path),
LanguageID: "go",
Version: 1,
Text: content,
},
})
if err != nil {
logger.Error("failed to call DidOpen", zap.Error(err))
}
}

func (gopls *Connection) DidClose(ctx context.Context, path string, lgoger *zap.Logger) {
err := gopls.server.DidClose(ctx, &protocol.DidCloseTextDocumentParams{
TextDocument: protocol.TextDocumentIdentifier{
URI: protocol.DocumentURI(path),
},
})
if err != nil {
lgoger.Error("failed to call DidClose", zap.Error(err))
}
}
29 changes: 29 additions & 0 deletions cmd/lint-panics/gopls/stream.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package gopls

import "io"

// CombinedReadWriteCloser combines stdin and stdout into one interface.
type CombinedReadWriteCloser struct {
stdin io.WriteCloser
stdout io.ReadCloser
}

// Write writes data to stdin.
func (c *CombinedReadWriteCloser) Write(p []byte) (n int, err error) {
return c.stdin.Write(p)
}

// Read reads data from stdout.
func (c *CombinedReadWriteCloser) Read(p []byte) (n int, err error) {
return c.stdout.Read(p)
}

// Close closes both stdin and stdout.
func (c *CombinedReadWriteCloser) Close() error {
err1 := c.stdin.Close()
err2 := c.stdout.Close()
if err1 != nil {
return err1
}
return err2
}
100 changes: 100 additions & 0 deletions cmd/lint-panics/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package main

import (
"os"
"time"
"context"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
gopls2 "github.com/status-im/status-go/cmd/lint-panics/gopls"
"github.com/status-im/status-go/cmd/lint-panics/processor"
"golang.org/x/tools/go/analysis/singlechecker"
"golang.org/x/tools/go/analysis"
"go/ast"
"fmt"
"path/filepath"
"strings"
)

func main() {
logger := buildLogger()

if len(os.Args) == 0 {
logger.Error("Usage: go run main.go <directory>")
os.Exit(1)
}

// Dirty hack to get the root directory for gopls
lastArg := os.Args[len(os.Args)-1]
dir, err := getRootAbsolutePath(lastArg)
if err != nil {
logger.Error("failed to get root directory", zap.Error(err))
os.Exit(1)
}

logger.Info("starting analysis...", zap.String("directory", dir))

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()

gopls := gopls2.NewGoplsClient(ctx, logger, dir)

analyzer := &analysis.Analyzer{
Name: "logpanics",
Doc: "reports missing defer call to LogOnPanic",
Run: func(pass *analysis.Pass) (interface{}, error) {
for _, file := range pass.Files {
p := processor.NewProcessor(logger, pass, gopls)
ast.Inspect(file, func(n ast.Node) bool {
p.ProcessNode(ctx, n)
return true
})
}
return nil, nil
},
}

singlechecker.Main(analyzer)
}

func buildLogger() *zap.Logger {
// Initialize logger with colors
loggerConfig := zap.NewDevelopmentConfig()
loggerConfig.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
loggerConfig.Level = zap.NewAtomicLevelAt(zap.ErrorLevel)
loggerConfig.Development = false
loggerConfig.DisableStacktrace = true
logger, err := loggerConfig.Build()
if err != nil {
fmt.Printf("failed to initialize logger: %s", err.Error())
os.Exit(1)
}

return logger.Named("main")
}

func getRootAbsolutePath(path string) (string, error) {
// Get the absolute path of the current working directory
workingDir, err := os.Getwd()
if err != nil {
return "", fmt.Errorf("failed to get working directory: %w", err)
}

if strings.HasSuffix(path, "...") {
path = strings.TrimSuffix(path, "...")
}

// Check if the given path is absolute
if !filepath.IsAbs(path) {
// If the path is not absolute, join it with the working directory
path = filepath.Join(workingDir, path)
}

// Convert the path to an absolute path (cleans the result)
absolutePath, err := filepath.Abs(path)
if err != nil {
return "", fmt.Errorf("failed to convert to absolute path: %w", err)
}

return absolutePath, nil
}
Loading

0 comments on commit 0ac1275

Please sign in to comment.