Add AST analysis tools and go run/test execution tools
This commit is contained in:
243
tool_analyze_goroutines.go
Normal file
243
tool_analyze_goroutines.go
Normal file
@@ -0,0 +1,243 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user