Files
gocp/tool_analyze_goroutines.go
2025-06-27 22:54:45 -07:00

243 lines
5.7 KiB
Go

package main
import (
"go/ast"
"go/token"
"strings"
)
type GoroutineUsage struct {
Position Position `json:"position"`
Function string `json:"function"`
InLoop bool `json:"in_loop"`
HasWaitGroup bool `json:"has_wait_group"`
Context string `json:"context"`
}
type GoroutineAnalysis struct {
Goroutines []GoroutineUsage `json:"goroutines"`
Issues []GoroutineIssue `json:"issues"`
}
type GoroutineIssue struct {
Type string `json:"type"`
Description string `json:"description"`
Position Position `json:"position"`
}
func analyzeGoroutines(dir string) (*GoroutineAnalysis, error) {
analysis := &GoroutineAnalysis{
Goroutines: []GoroutineUsage{},
Issues: []GoroutineIssue{},
}
err := walkGoFiles(dir, func(path string, src []byte, file *ast.File, fset *token.FileSet) error {
// Track WaitGroup usage
waitGroupVars := make(map[string]bool)
hasWaitGroupImport := false
// Check imports
for _, imp := range file.Imports {
if imp.Path != nil && imp.Path.Value == `"sync"` {
hasWaitGroupImport = true
break
}
}
// First pass: find WaitGroup variables
ast.Inspect(file, func(n ast.Node) bool {
switch node := n.(type) {
case *ast.ValueSpec:
for i, name := range node.Names {
if i < len(node.Values) {
if isWaitGroupType(node.Type) || isWaitGroupExpr(node.Values[i]) {
waitGroupVars[name.Name] = true
}
}
}
case *ast.AssignStmt:
for i, lhs := range node.Lhs {
if ident, ok := lhs.(*ast.Ident); ok && i < len(node.Rhs) {
if isWaitGroupExpr(node.Rhs[i]) {
waitGroupVars[ident.Name] = true
}
}
}
}
return true
})
// Second pass: analyze goroutines
ast.Inspect(file, func(n ast.Node) bool {
if goStmt, ok := n.(*ast.GoStmt); ok {
pos := fset.Position(goStmt.Pos())
funcName := extractFunctionName(goStmt.Call)
inLoop := isInLoop(file, goStmt)
hasWG := hasNearbyWaitGroup(file, goStmt, waitGroupVars)
usage := GoroutineUsage{
Position: newPosition(pos),
Function: funcName,
InLoop: inLoop,
HasWaitGroup: hasWG,
Context: extractContext(src, pos),
}
analysis.Goroutines = append(analysis.Goroutines, usage)
// Check for issues
if inLoop && !hasWG {
issue := GoroutineIssue{
Type: "goroutine_leak_risk",
Description: "Goroutine launched in loop without WaitGroup may cause resource leak",
Position: newPosition(pos),
}
analysis.Issues = append(analysis.Issues, issue)
}
// Check for goroutines without synchronization
if !hasWG && !hasChannelCommunication(goStmt.Call) && hasWaitGroupImport {
issue := GoroutineIssue{
Type: "missing_synchronization",
Description: "Goroutine launched without apparent synchronization mechanism",
Position: newPosition(pos),
}
analysis.Issues = append(analysis.Issues, issue)
}
}
return true
})
return nil
})
return analysis, err
}
func isWaitGroupType(expr ast.Expr) bool {
if expr == nil {
return false
}
if sel, ok := expr.(*ast.SelectorExpr); ok {
if ident, ok := sel.X.(*ast.Ident); ok {
return ident.Name == "sync" && sel.Sel.Name == "WaitGroup"
}
}
return false
}
func isWaitGroupExpr(expr ast.Expr) bool {
switch e := expr.(type) {
case *ast.CompositeLit:
return isWaitGroupType(e.Type)
case *ast.UnaryExpr:
if e.Op == token.AND {
return isWaitGroupExpr(e.X)
}
}
return false
}
func extractFunctionName(call *ast.CallExpr) string {
switch fun := call.Fun.(type) {
case *ast.Ident:
return fun.Name
case *ast.SelectorExpr:
return exprToString(fun.X) + "." + fun.Sel.Name
case *ast.FuncLit:
return "anonymous function"
default:
return "unknown"
}
}
func isInLoop(file *ast.File, target ast.Node) bool {
var inLoop bool
ast.Inspect(file, func(n ast.Node) bool {
switch n.(type) {
case *ast.ForStmt, *ast.RangeStmt:
// Check if target is within this loop
if containsNode(n, target) {
inLoop = true
return false
}
}
return true
})
return inLoop
}
func containsNode(parent, child ast.Node) bool {
var found bool
ast.Inspect(parent, func(n ast.Node) bool {
if n == child {
found = true
return false
}
return true
})
return found
}
func hasNearbyWaitGroup(file *ast.File, goStmt *ast.GoStmt, waitGroupVars map[string]bool) bool {
// Look for WaitGroup.Add calls in the same block or parent function
var hasWG bool
ast.Inspect(file, func(n ast.Node) bool {
switch node := n.(type) {
case *ast.CallExpr:
if sel, ok := node.Fun.(*ast.SelectorExpr); ok {
if ident, ok := sel.X.(*ast.Ident); ok {
if waitGroupVars[ident.Name] && sel.Sel.Name == "Add" {
// Check if this Add call is near the goroutine
if isNearby(file, node, goStmt) {
hasWG = true
return false
}
}
}
}
}
return true
})
return hasWG
}
func isNearby(file *ast.File, node1, node2 ast.Node) bool {
// Simple proximity check - in same function
var func1, func2 *ast.FuncDecl
ast.Inspect(file, func(n ast.Node) bool {
if fn, ok := n.(*ast.FuncDecl); ok {
if containsNode(fn, node1) {
func1 = fn
}
if containsNode(fn, node2) {
func2 = fn
}
}
return true
})
return func1 == func2 && func1 != nil
}
func hasChannelCommunication(call *ast.CallExpr) bool {
// Check if the function likely uses channels for synchronization
hasChannel := false
ast.Inspect(call, func(n ast.Node) bool {
switch n.(type) {
case *ast.ChanType, *ast.SendStmt:
hasChannel = true
return false
}
if ident, ok := n.(*ast.Ident); ok {
if strings.Contains(strings.ToLower(ident.Name), "chan") {
hasChannel = true
return false
}
}
return true
})
return hasChannel
}