246 lines
6.1 KiB
Go
246 lines
6.1 KiB
Go
package analyzer
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"go/ast"
|
|
"os"
|
|
|
|
"go.uber.org/zap"
|
|
|
|
goparser "go/parser"
|
|
gotoken "go/token"
|
|
"strings"
|
|
|
|
"github.com/pkg/errors"
|
|
"golang.org/x/tools/go/analysis"
|
|
"golang.org/x/tools/go/analysis/passes/inspect"
|
|
"golang.org/x/tools/go/ast/inspector"
|
|
|
|
"github.com/status-im/status-go/cmd/lint-panics/gopls"
|
|
"github.com/status-im/status-go/cmd/lint-panics/utils"
|
|
)
|
|
|
|
const Pattern = "LogOnPanic"
|
|
|
|
type Analyzer struct {
|
|
logger *zap.Logger
|
|
lsp LSP
|
|
cfg *Config
|
|
}
|
|
|
|
type LSP interface {
|
|
Definition(context.Context, string, int, int) (string, int, error)
|
|
}
|
|
|
|
func New(ctx context.Context, logger *zap.Logger) (*analysis.Analyzer, error) {
|
|
cfg := Config{}
|
|
flags, err := cfg.ParseFlags()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
logger.Info("creating analyzer", zap.String("root", cfg.RootDir))
|
|
|
|
goplsClient := gopls.NewGoplsClient(ctx, logger, cfg.RootDir)
|
|
processor := newAnalyzer(logger, goplsClient, &cfg)
|
|
|
|
analyzer := &analysis.Analyzer{
|
|
Name: "logpanics",
|
|
Doc: fmt.Sprintf("reports missing defer call to %s", Pattern),
|
|
Flags: flags,
|
|
Requires: []*analysis.Analyzer{inspect.Analyzer},
|
|
Run: func(pass *analysis.Pass) (interface{}, error) {
|
|
return processor.Run(ctx, pass)
|
|
},
|
|
}
|
|
|
|
return analyzer, nil
|
|
}
|
|
|
|
func newAnalyzer(logger *zap.Logger, lsp LSP, cfg *Config) *Analyzer {
|
|
return &Analyzer{
|
|
logger: logger.Named("processor"),
|
|
lsp: lsp,
|
|
cfg: cfg.WithAbsolutePaths(),
|
|
}
|
|
}
|
|
|
|
func (p *Analyzer) Run(ctx context.Context, pass *analysis.Pass) (interface{}, error) {
|
|
inspected, ok := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
|
|
if !ok {
|
|
return nil, errors.New("analyzer is not type *inspector.Inspector")
|
|
}
|
|
|
|
// Create a nodes filter for goroutines (GoStmt represents a 'go' statement)
|
|
nodeFilter := []ast.Node{
|
|
(*ast.GoStmt)(nil),
|
|
}
|
|
|
|
// Inspect go statements
|
|
inspected.Preorder(nodeFilter, func(n ast.Node) {
|
|
p.ProcessNode(ctx, pass, n)
|
|
})
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
func (p *Analyzer) ProcessNode(ctx context.Context, pass *analysis.Pass, n ast.Node) {
|
|
goStmt, ok := n.(*ast.GoStmt)
|
|
if !ok {
|
|
panic("unexpected node type")
|
|
}
|
|
|
|
switch fun := goStmt.Call.Fun.(type) {
|
|
case *ast.FuncLit: // anonymous function
|
|
pos := pass.Fset.Position(fun.Pos())
|
|
logger := p.logger.With(
|
|
utils.ZapURI(pos.Filename, pos.Line),
|
|
zap.Int("column", pos.Column),
|
|
)
|
|
|
|
logger.Debug("found anonymous goroutine")
|
|
if err := p.checkGoroutine(fun.Body); err != nil {
|
|
p.logLinterError(pass, fun.Pos(), fun.Pos(), err)
|
|
}
|
|
|
|
case *ast.SelectorExpr: // method call
|
|
pos := pass.Fset.Position(fun.Sel.Pos())
|
|
p.logger.Info("found method call as goroutine",
|
|
zap.String("methodName", fun.Sel.Name),
|
|
utils.ZapURI(pos.Filename, pos.Line),
|
|
zap.Int("column", pos.Column),
|
|
)
|
|
|
|
defPos, err := p.checkGoroutineDefinition(ctx, pos, pass)
|
|
if err != nil {
|
|
p.logLinterError(pass, defPos, fun.Sel.Pos(), err)
|
|
}
|
|
|
|
case *ast.Ident: // function call
|
|
pos := pass.Fset.Position(fun.Pos())
|
|
p.logger.Info("found function call as goroutine",
|
|
zap.String("functionName", fun.Name),
|
|
utils.ZapURI(pos.Filename, pos.Line),
|
|
zap.Int("column", pos.Column),
|
|
)
|
|
|
|
defPos, err := p.checkGoroutineDefinition(ctx, pos, pass)
|
|
if err != nil {
|
|
p.logLinterError(pass, defPos, fun.Pos(), err)
|
|
}
|
|
|
|
default:
|
|
p.logger.Error("unexpected goroutine type",
|
|
zap.String("type", fmt.Sprintf("%T", fun)),
|
|
)
|
|
}
|
|
}
|
|
|
|
func (p *Analyzer) parseFile(path string, pass *analysis.Pass) (*ast.File, error) {
|
|
logger := p.logger.With(zap.String("path", path))
|
|
|
|
src, err := os.ReadFile(path)
|
|
if err != nil {
|
|
logger.Error("failed to open file", zap.Error(err))
|
|
}
|
|
|
|
file, err := goparser.ParseFile(pass.Fset, path, src, 0)
|
|
if err != nil {
|
|
logger.Error("failed to parse file", zap.Error(err))
|
|
return nil, err
|
|
}
|
|
|
|
return file, nil
|
|
}
|
|
|
|
func (p *Analyzer) checkGoroutine(body *ast.BlockStmt) error {
|
|
if body == nil {
|
|
p.logger.Warn("missing function body")
|
|
return nil
|
|
}
|
|
if len(body.List) == 0 {
|
|
// empty goroutine is weird, but it never panics, so not a linter error
|
|
return nil
|
|
}
|
|
|
|
deferStatement, ok := body.List[0].(*ast.DeferStmt)
|
|
if !ok {
|
|
return errors.New("first statement is not defer")
|
|
}
|
|
|
|
selectorExpr, ok := deferStatement.Call.Fun.(*ast.SelectorExpr)
|
|
if !ok {
|
|
return errors.New("first statement call is not a selector")
|
|
}
|
|
|
|
firstLineFunName := selectorExpr.Sel.Name
|
|
if firstLineFunName != Pattern {
|
|
return errors.Errorf("first statement is not %s", Pattern)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *Analyzer) getFunctionBody(node ast.Node, lineNumber int, pass *analysis.Pass) (body *ast.BlockStmt, pos gotoken.Pos) {
|
|
ast.Inspect(node, func(n ast.Node) bool {
|
|
// Check if the node is a function declaration
|
|
funcDecl, ok := n.(*ast.FuncDecl)
|
|
if !ok {
|
|
return true
|
|
}
|
|
|
|
if pass.Fset.Position(n.Pos()).Line != lineNumber {
|
|
return true
|
|
}
|
|
|
|
body = funcDecl.Body
|
|
pos = n.Pos()
|
|
return false
|
|
})
|
|
|
|
return body, pos
|
|
|
|
}
|
|
|
|
func (p *Analyzer) checkGoroutineDefinition(ctx context.Context, pos gotoken.Position, pass *analysis.Pass) (gotoken.Pos, error) {
|
|
defFilePath, defLineNumber, err := p.lsp.Definition(ctx, pos.Filename, pos.Line, pos.Column)
|
|
if err != nil {
|
|
p.logger.Error("failed to find function definition", zap.Error(err))
|
|
return 0, err
|
|
}
|
|
|
|
file, err := p.parseFile(defFilePath, pass)
|
|
if err != nil {
|
|
p.logger.Error("failed to parse file", zap.Error(err))
|
|
return 0, err
|
|
}
|
|
|
|
body, defPosition := p.getFunctionBody(file, defLineNumber, pass)
|
|
return defPosition, p.checkGoroutine(body)
|
|
}
|
|
|
|
func (p *Analyzer) logLinterError(pass *analysis.Pass, errPos gotoken.Pos, callPos gotoken.Pos, err error) {
|
|
errPosition := pass.Fset.Position(errPos)
|
|
callPosition := pass.Fset.Position(callPos)
|
|
|
|
if p.skip(errPosition.Filename) || p.skip(callPosition.Filename) {
|
|
return
|
|
}
|
|
|
|
message := fmt.Sprintf("missing %s()", Pattern)
|
|
p.logger.Warn(message,
|
|
utils.ZapURI(errPosition.Filename, errPosition.Line),
|
|
zap.String("details", err.Error()))
|
|
|
|
if callPos == errPos {
|
|
pass.Reportf(errPos, "missing defer call to %s", Pattern)
|
|
} else {
|
|
pass.Reportf(callPos, "missing defer call to %s", Pattern)
|
|
}
|
|
}
|
|
|
|
func (p *Analyzer) skip(filepath string) bool {
|
|
return p.cfg.SkipDir != "" && strings.HasPrefix(filepath, p.cfg.SkipDir)
|
|
}
|