status-go/check-goroutines/check_defer_log_on_panic.go

226 lines
6.7 KiB
Go

package main
import (
"bufio"
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
"github.com/ethereum/go-ethereum/log"
)
func main() {
if len(os.Args) < 2 {
fmt.Println("Usage: go run main.go <directory>")
return
}
// Initialize logger with colors
handler := log.StreamHandler(os.Stdout, log.TerminalFormat(true))
log.Root().SetHandler(log.LvlFilterHandler(log.LvlInfo, handler))
dir := os.Args[1]
log.Info("Starting analysis...", "directory", dir)
// Step 1: Scan all files and look for `go` calls
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
log.Error("Error walking the path", "path", dir, "error", err)
return err
}
if info.IsDir() || !strings.HasSuffix(info.Name(), ".go") {
return nil
}
log.Info("Scanning Go file", "file", path)
checkFileForGoroutines(path)
return nil
})
if err != nil {
log.Error("Error during file walk", "error", err)
}
log.Info("Analysis complete")
}
// checkFileForGoroutines scans a Go file for any `go` statements (goroutines)
func checkFileForGoroutines(filePath string) {
file, err := os.Open(filePath)
if err != nil {
log.Error("Error opening file", "file", filePath, "error", err)
return
}
defer file.Close()
scanner := bufio.NewScanner(file)
var lineNumber int
// Regex for non-anonymous function/method calls: `go functionName()`
regex := regexp.MustCompile(`go\s+(\.|\w)+\(\)$`)
for scanner.Scan() {
lineNumber++
line := scanner.Text() // Do not trim spaces here
// Detect anonymous goroutines
if strings.Contains(line, "go func") {
log.Info("Found anonymous goroutine", "file", filePath, "line", lineNumber, "lineContent", line)
checkAnonymousGoroutine(filePath, lineNumber)
continue
}
// Detect non-anonymous goroutines using regex
if regex.MatchString(line) {
log.Info("Found non-anonymous goroutine", "file", filePath, "line", lineNumber, "lineContent", line)
// Find the position of the first occurrence of "()"
cursorPos := strings.Index(line, "()")
if cursorPos == -1 {
log.Error("Failed to find function call", "file", filePath, "line", lineNumber)
continue
}
// Calculate the cursor position by adjusting for tabs (counting tabs as 4 characters)
//tabs := strings.Count(line[:cursorPos], "\t")
//adjustedCursorPos := cursorPos - (tabs * 4) // Subtract 3 for each tab since a tab counts as 4 chars
checkNamedFunction(filePath, lineNumber, cursorPos)
}
}
if err := scanner.Err(); err != nil {
log.Error("Error reading file", "file", filePath, "error", err)
}
}
// checkAnonymousGoroutine checks if an anonymous goroutine has `defer utils.LogOnPanic()`
func checkAnonymousGoroutine(filePath string, lineNumber int) {
//log.Debug("Checking anonymous goroutine", "file", filePath, "line", lineNumber)
// Open the file again and scan from the `go func` line onwards
file, err := os.Open(filePath)
if err != nil {
log.Error("Error opening file", "file", filePath, "error", err)
return
}
defer file.Close()
scanner := bufio.NewScanner(file)
var currentLine int
for scanner.Scan() {
currentLine++
if currentLine <= lineNumber {
continue
}
line := scanner.Text()
// First line of the function body
if strings.Contains(line, "defer utils.LogOnPanic()") {
log.Info("Found defer utils.LogOnPanic() in anonymous function", "file", filePath, "line", lineNumber)
} else {
log.Warn("Missing defer utils.LogOnPanic() in anonymous function", "file", filePath, "line", lineNumber)
}
return
}
if err := scanner.Err(); err != nil {
log.Error("Error reading file", "file", filePath, "error", err)
}
}
// extractFunctionNameAndCursorPosition extracts the function or method name and calculates the cursor position
func extractFunctionNameAndCursorPosition(line string, matches []string) (string, int) {
funcName := matches[1]
// Calculate the cursor position (count tabs as one character)
regex := regexp.MustCompile(`\bgo\s+`)
cursorPos := regex.FindStringIndex(line)[1] // Position after `go ` keyword
return funcName, cursorPos
}
// checkNamedFunction uses `gopls` to find the definition of a named function/method and checks its first line
func checkNamedFunction(filePath string, lineNumber, charPos int) {
log.Debug("Checking named function", "file", filePath, "line", lineNumber, "char", charPos)
// Use `gopls` to find the definition of the function/method
cmd := exec.Command("gopls", "definition", fmt.Sprintf("%s:%d:%d", filePath, lineNumber, charPos))
output, err := cmd.Output()
if err != nil {
log.Error("Error running gopls definition", "file", filePath, "line", lineNumber, "error", err)
return
}
definitionOutput := string(output)
// Parse the definition output to find the file and line number of the function definition
parseAndCheckFunctionDefinition(definitionOutput)
}
// parseAndCheckFunctionDefinition parses the output of `gopls definition` and checks the function body
func parseAndCheckFunctionDefinition(definitionOutput string) {
// The output of `gopls definition` will contain the file path and position of the function definition
// Example output might be:
// /path/to/file.go:23:5
log.Debug("Parsed definition", "definition", definitionOutput)
// Extract file path and line number from the output
parts := strings.Split(definitionOutput, ":")
if len(parts) < 2 {
log.Error("Failed to parse gopls definition output", "output", definitionOutput)
return
}
defFilePath := parts[0]
lineNumber := atoi(parts[1])
// Open the file and check the first statement inside the function body
checkFirstLineInFunctionBody(defFilePath, lineNumber)
}
// checkFirstLineInFunctionBody checks the first line inside a function body for `defer utils.LogOnPanic()`
func checkFirstLineInFunctionBody(filePath string, startLine int) {
log.Debug("Checking function body", "file", filePath, "startLine", startLine)
file, err := os.Open(filePath)
if err != nil {
log.Error("Error opening file", "file", filePath, "error", err)
return
}
defer file.Close()
scanner := bufio.NewScanner(file)
var currentLine int
for scanner.Scan() {
currentLine++
if currentLine <= startLine {
continue
}
line := scanner.Text()
if strings.Contains(line, "defer utils.LogOnPanic()") {
log.Info("Found defer utils.LogOnPanic() in function", "file", filePath, "line", startLine)
} else {
log.Warn("Missing defer utils.LogOnPanic() in function", "file", filePath, "line", startLine)
}
return
}
if err := scanner.Err(); err != nil {
log.Error("Error reading file", "file", filePath, "error", err)
}
}
// atoi is a helper to safely convert a string to an int
func atoi(s string) int {
i, err := strconv.Atoi(s)
if err != nil {
return 0
}
return i
}