305 lines
7.9 KiB
Go
305 lines
7.9 KiB
Go
|
|
package main
|
||
|
|
|
||
|
|
import (
|
||
|
|
"go/ast"
|
||
|
|
"go/token"
|
||
|
|
"strings"
|
||
|
|
)
|
||
|
|
|
||
|
|
type DeferUsage struct {
|
||
|
|
Statement string `json:"statement"`
|
||
|
|
Position Position `json:"position"`
|
||
|
|
InLoop bool `json:"in_loop"`
|
||
|
|
InFunction string `json:"in_function"`
|
||
|
|
Context string `json:"context"`
|
||
|
|
}
|
||
|
|
|
||
|
|
type DeferAnalysis struct {
|
||
|
|
Defers []DeferUsage `json:"defers"`
|
||
|
|
Issues []DeferIssue `json:"issues"`
|
||
|
|
}
|
||
|
|
|
||
|
|
type DeferIssue struct {
|
||
|
|
Type string `json:"type"`
|
||
|
|
Description string `json:"description"`
|
||
|
|
Position Position `json:"position"`
|
||
|
|
}
|
||
|
|
|
||
|
|
func analyzeDeferPatterns(dir string) (*DeferAnalysis, error) {
|
||
|
|
analysis := &DeferAnalysis{
|
||
|
|
Defers: []DeferUsage{},
|
||
|
|
Issues: []DeferIssue{},
|
||
|
|
}
|
||
|
|
|
||
|
|
err := walkGoFiles(dir, func(path string, src []byte, file *ast.File, fset *token.FileSet) error {
|
||
|
|
var currentFunc string
|
||
|
|
|
||
|
|
ast.Inspect(file, func(n ast.Node) bool {
|
||
|
|
switch node := n.(type) {
|
||
|
|
case *ast.FuncDecl:
|
||
|
|
currentFunc = node.Name.Name
|
||
|
|
analyzeFunctionDefers(node, fset, src, analysis)
|
||
|
|
|
||
|
|
case *ast.FuncLit:
|
||
|
|
currentFunc = "anonymous function"
|
||
|
|
analyzeFunctionDefers(&ast.FuncDecl{Body: node.Body}, fset, src, analysis)
|
||
|
|
|
||
|
|
case *ast.DeferStmt:
|
||
|
|
pos := fset.Position(node.Pos())
|
||
|
|
usage := DeferUsage{
|
||
|
|
Statement: extractDeferStatement(node),
|
||
|
|
Position: newPosition(pos),
|
||
|
|
InLoop: isInLoop(file, node),
|
||
|
|
InFunction: currentFunc,
|
||
|
|
Context: extractContext(src, pos),
|
||
|
|
}
|
||
|
|
analysis.Defers = append(analysis.Defers, usage)
|
||
|
|
|
||
|
|
// Check for issues
|
||
|
|
if usage.InLoop {
|
||
|
|
issue := DeferIssue{
|
||
|
|
Type: "defer_in_loop",
|
||
|
|
Description: "defer in loop will accumulate until function returns",
|
||
|
|
Position: newPosition(pos),
|
||
|
|
}
|
||
|
|
analysis.Issues = append(analysis.Issues, issue)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check for defer of result of function call
|
||
|
|
if hasNestedCall(node.Call) {
|
||
|
|
issue := DeferIssue{
|
||
|
|
Type: "defer_nested_call",
|
||
|
|
Description: "defer evaluates function arguments immediately - nested calls execute now",
|
||
|
|
Position: newPosition(pos),
|
||
|
|
}
|
||
|
|
analysis.Issues = append(analysis.Issues, issue)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check for useless defer patterns
|
||
|
|
checkUselessDefer(node, file, fset, analysis)
|
||
|
|
}
|
||
|
|
return true
|
||
|
|
})
|
||
|
|
|
||
|
|
return nil
|
||
|
|
})
|
||
|
|
|
||
|
|
return analysis, err
|
||
|
|
}
|
||
|
|
|
||
|
|
func extractDeferStatement(deferStmt *ast.DeferStmt) string {
|
||
|
|
switch call := deferStmt.Call.Fun.(type) {
|
||
|
|
case *ast.Ident:
|
||
|
|
return "defer " + call.Name + "(...)"
|
||
|
|
case *ast.SelectorExpr:
|
||
|
|
return "defer " + exprToString(call) + "(...)"
|
||
|
|
case *ast.FuncLit:
|
||
|
|
return "defer func() { ... }"
|
||
|
|
default:
|
||
|
|
return "defer <unknown>"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func hasNestedCall(call *ast.CallExpr) bool {
|
||
|
|
// Check if any argument is a function call
|
||
|
|
for _, arg := range call.Args {
|
||
|
|
if _, ok := arg.(*ast.CallExpr); ok {
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
func analyzeFunctionDefers(fn *ast.FuncDecl, fset *token.FileSet, src []byte, analysis *DeferAnalysis) {
|
||
|
|
if fn.Body == nil {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
var defers []*ast.DeferStmt
|
||
|
|
var hasReturn bool
|
||
|
|
var returnPos token.Position
|
||
|
|
|
||
|
|
// Collect all defers and check for early returns
|
||
|
|
ast.Inspect(fn.Body, func(n ast.Node) bool {
|
||
|
|
switch node := n.(type) {
|
||
|
|
case *ast.DeferStmt:
|
||
|
|
defers = append(defers, node)
|
||
|
|
case *ast.ReturnStmt:
|
||
|
|
hasReturn = true
|
||
|
|
returnPos = fset.Position(node.Pos())
|
||
|
|
case *ast.FuncLit:
|
||
|
|
// Don't analyze nested functions
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
return true
|
||
|
|
})
|
||
|
|
|
||
|
|
// Check defer ordering issues
|
||
|
|
if len(defers) > 1 {
|
||
|
|
checkDeferOrdering(defers, fset, analysis)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check for defer after return path
|
||
|
|
if hasReturn {
|
||
|
|
for _, def := range defers {
|
||
|
|
defPos := fset.Position(def.Pos())
|
||
|
|
if defPos.Line > returnPos.Line {
|
||
|
|
issue := DeferIssue{
|
||
|
|
Type: "unreachable_defer",
|
||
|
|
Description: "defer statement after return is unreachable",
|
||
|
|
Position: newPosition(defPos),
|
||
|
|
}
|
||
|
|
analysis.Issues = append(analysis.Issues, issue)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check for missing defer on resource cleanup
|
||
|
|
checkMissingDefers(fn, fset, analysis)
|
||
|
|
}
|
||
|
|
|
||
|
|
func checkDeferOrdering(defers []*ast.DeferStmt, fset *token.FileSet, analysis *DeferAnalysis) {
|
||
|
|
// Check for dependent defers in wrong order
|
||
|
|
for i := 0; i < len(defers)-1; i++ {
|
||
|
|
for j := i + 1; j < len(defers); j++ {
|
||
|
|
if areDefersDependentWrongOrder(defers[i], defers[j]) {
|
||
|
|
pos := fset.Position(defers[j].Pos())
|
||
|
|
issue := DeferIssue{
|
||
|
|
Type: "defer_order_issue",
|
||
|
|
Description: "defer statements may execute in wrong order (LIFO)",
|
||
|
|
Position: newPosition(pos),
|
||
|
|
}
|
||
|
|
analysis.Issues = append(analysis.Issues, issue)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func areDefersDependentWrongOrder(first, second *ast.DeferStmt) bool {
|
||
|
|
// Simple heuristic: check for Close() after Flush() or similar patterns
|
||
|
|
firstName := extractMethodName(first.Call)
|
||
|
|
secondName := extractMethodName(second.Call)
|
||
|
|
|
||
|
|
// Common patterns where order matters
|
||
|
|
orderPatterns := map[string]string{
|
||
|
|
"Flush": "Close",
|
||
|
|
"Unlock": "Lock",
|
||
|
|
"Done": "Add",
|
||
|
|
}
|
||
|
|
|
||
|
|
for before, after := range orderPatterns {
|
||
|
|
if firstName == after && secondName == before {
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
func extractMethodName(call *ast.CallExpr) string {
|
||
|
|
switch fun := call.Fun.(type) {
|
||
|
|
case *ast.Ident:
|
||
|
|
return fun.Name
|
||
|
|
case *ast.SelectorExpr:
|
||
|
|
return fun.Sel.Name
|
||
|
|
}
|
||
|
|
return ""
|
||
|
|
}
|
||
|
|
|
||
|
|
func checkUselessDefer(deferStmt *ast.DeferStmt, file *ast.File, fset *token.FileSet, analysis *DeferAnalysis) {
|
||
|
|
// Check if defer is the last statement before return
|
||
|
|
ast.Inspect(file, func(n ast.Node) bool {
|
||
|
|
if block, ok := n.(*ast.BlockStmt); ok {
|
||
|
|
for i, stmt := range block.List {
|
||
|
|
if stmt == deferStmt && i < len(block.List)-1 {
|
||
|
|
// Check if next statement is return
|
||
|
|
if _, ok := block.List[i+1].(*ast.ReturnStmt); ok {
|
||
|
|
pos := fset.Position(deferStmt.Pos())
|
||
|
|
issue := DeferIssue{
|
||
|
|
Type: "unnecessary_defer",
|
||
|
|
Description: "defer immediately before return is unnecessary",
|
||
|
|
Position: newPosition(pos),
|
||
|
|
}
|
||
|
|
analysis.Issues = append(analysis.Issues, issue)
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return true
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func checkMissingDefers(fn *ast.FuncDecl, fset *token.FileSet, analysis *DeferAnalysis) {
|
||
|
|
// Look for resource acquisition without corresponding defer
|
||
|
|
resources := make(map[string]token.Position) // resource var -> position
|
||
|
|
deferred := make(map[string]bool)
|
||
|
|
|
||
|
|
ast.Inspect(fn.Body, func(n ast.Node) bool {
|
||
|
|
switch node := n.(type) {
|
||
|
|
case *ast.AssignStmt:
|
||
|
|
// Check for resource acquisition
|
||
|
|
for i, lhs := range node.Lhs {
|
||
|
|
if ident, ok := lhs.(*ast.Ident); ok && i < len(node.Rhs) {
|
||
|
|
if isResourceAcquisition(node.Rhs[i]) {
|
||
|
|
resources[ident.Name] = fset.Position(node.Pos())
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
case *ast.DeferStmt:
|
||
|
|
// Check if defer releases a resource
|
||
|
|
if varName := extractDeferredResourceVar(node.Call); varName != "" {
|
||
|
|
deferred[varName] = true
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return true
|
||
|
|
})
|
||
|
|
|
||
|
|
// Report resources without defers
|
||
|
|
for resource, pos := range resources {
|
||
|
|
if !deferred[resource] {
|
||
|
|
issue := DeferIssue{
|
||
|
|
Type: "missing_defer",
|
||
|
|
Description: "Resource '" + resource + "' acquired but not deferred for cleanup",
|
||
|
|
Position: newPosition(pos),
|
||
|
|
}
|
||
|
|
analysis.Issues = append(analysis.Issues, issue)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func isResourceAcquisition(expr ast.Expr) bool {
|
||
|
|
call, ok := expr.(*ast.CallExpr)
|
||
|
|
if !ok {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check for common resource acquisition patterns
|
||
|
|
switch fun := call.Fun.(type) {
|
||
|
|
case *ast.SelectorExpr:
|
||
|
|
method := fun.Sel.Name
|
||
|
|
resourceMethods := []string{"Open", "Create", "Dial", "Connect", "Lock", "RLock", "Begin"}
|
||
|
|
for _, rm := range resourceMethods {
|
||
|
|
if method == rm || strings.HasPrefix(method, "Open") || strings.HasPrefix(method, "New") {
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
func extractDeferredResourceVar(call *ast.CallExpr) string {
|
||
|
|
// Extract the variable being cleaned up in defer
|
||
|
|
switch fun := call.Fun.(type) {
|
||
|
|
case *ast.SelectorExpr:
|
||
|
|
if ident, ok := fun.X.(*ast.Ident); ok {
|
||
|
|
method := fun.Sel.Name
|
||
|
|
if method == "Close" || method == "Unlock" || method == "RUnlock" ||
|
||
|
|
method == "Done" || method == "Release" {
|
||
|
|
return ident.Name
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return ""
|
||
|
|
}
|