status-go/cmd/lint-panics/analyzer/analyzer.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)
}