374 lines
10 KiB
Go
374 lines
10 KiB
Go
|
|
package main
|
||
|
|
|
||
|
|
import (
|
||
|
|
"go/ast"
|
||
|
|
"go/token"
|
||
|
|
"strings"
|
||
|
|
)
|
||
|
|
|
||
|
|
type MemoryAllocation struct {
|
||
|
|
Type string `json:"type"` // "make", "new", "composite", "append", "string_concat"
|
||
|
|
Description string `json:"description"`
|
||
|
|
InLoop bool `json:"in_loop"`
|
||
|
|
Position Position `json:"position"`
|
||
|
|
Context string `json:"context"`
|
||
|
|
}
|
||
|
|
|
||
|
|
type AllocationAnalysis struct {
|
||
|
|
Allocations []MemoryAllocation `json:"allocations"`
|
||
|
|
Issues []AllocationIssue `json:"issues"`
|
||
|
|
}
|
||
|
|
|
||
|
|
type AllocationIssue struct {
|
||
|
|
Type string `json:"type"`
|
||
|
|
Description string `json:"description"`
|
||
|
|
Position Position `json:"position"`
|
||
|
|
}
|
||
|
|
|
||
|
|
func analyzeMemoryAllocations(dir string) (*AllocationAnalysis, error) {
|
||
|
|
analysis := &AllocationAnalysis{
|
||
|
|
Allocations: []MemoryAllocation{},
|
||
|
|
Issues: []AllocationIssue{},
|
||
|
|
}
|
||
|
|
|
||
|
|
err := walkGoFiles(dir, func(path string, src []byte, file *ast.File, fset *token.FileSet) error {
|
||
|
|
// Analyze allocations
|
||
|
|
ast.Inspect(file, func(n ast.Node) bool {
|
||
|
|
switch node := n.(type) {
|
||
|
|
case *ast.CallExpr:
|
||
|
|
analyzeCallExpr(node, file, fset, src, analysis)
|
||
|
|
|
||
|
|
case *ast.CompositeLit:
|
||
|
|
pos := fset.Position(node.Pos())
|
||
|
|
alloc := MemoryAllocation{
|
||
|
|
Type: "composite",
|
||
|
|
Description: "Composite literal: " + exprToString(node.Type),
|
||
|
|
InLoop: isInLoop(file, node),
|
||
|
|
Position: newPosition(pos),
|
||
|
|
Context: extractContext(src, pos),
|
||
|
|
}
|
||
|
|
analysis.Allocations = append(analysis.Allocations, alloc)
|
||
|
|
|
||
|
|
if alloc.InLoop {
|
||
|
|
issue := AllocationIssue{
|
||
|
|
Type: "allocation_in_loop",
|
||
|
|
Description: "Composite literal allocation inside loop",
|
||
|
|
Position: newPosition(pos),
|
||
|
|
}
|
||
|
|
analysis.Issues = append(analysis.Issues, issue)
|
||
|
|
}
|
||
|
|
|
||
|
|
case *ast.BinaryExpr:
|
||
|
|
if node.Op == token.ADD {
|
||
|
|
analyzeStringConcat(node, file, fset, src, analysis)
|
||
|
|
}
|
||
|
|
|
||
|
|
case *ast.UnaryExpr:
|
||
|
|
if node.Op == token.AND {
|
||
|
|
// Taking address of value causes allocation
|
||
|
|
pos := fset.Position(node.Pos())
|
||
|
|
if isEscaping(file, node) {
|
||
|
|
alloc := MemoryAllocation{
|
||
|
|
Type: "address_of",
|
||
|
|
Description: "Taking address of value (escapes to heap)",
|
||
|
|
InLoop: isInLoop(file, node),
|
||
|
|
Position: newPosition(pos),
|
||
|
|
Context: extractContext(src, pos),
|
||
|
|
}
|
||
|
|
analysis.Allocations = append(analysis.Allocations, alloc)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return true
|
||
|
|
})
|
||
|
|
|
||
|
|
// Look for specific patterns
|
||
|
|
findAllocationPatterns(file, fset, src, analysis)
|
||
|
|
|
||
|
|
return nil
|
||
|
|
})
|
||
|
|
|
||
|
|
return analysis, err
|
||
|
|
}
|
||
|
|
|
||
|
|
func analyzeCallExpr(call *ast.CallExpr, file *ast.File, fset *token.FileSet, src []byte, analysis *AllocationAnalysis) {
|
||
|
|
ident, ok := call.Fun.(*ast.Ident)
|
||
|
|
if !ok {
|
||
|
|
// Check for method calls like strings.Builder
|
||
|
|
if sel, ok := call.Fun.(*ast.SelectorExpr); ok {
|
||
|
|
analyzeMethodCall(sel, call, file, fset, src, analysis)
|
||
|
|
}
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
pos := fset.Position(call.Pos())
|
||
|
|
inLoop := isInLoop(file, call)
|
||
|
|
|
||
|
|
switch ident.Name {
|
||
|
|
case "make":
|
||
|
|
if len(call.Args) > 0 {
|
||
|
|
typeStr := exprToString(call.Args[0])
|
||
|
|
sizeStr := ""
|
||
|
|
if len(call.Args) > 1 {
|
||
|
|
sizeStr = " with size"
|
||
|
|
}
|
||
|
|
|
||
|
|
alloc := MemoryAllocation{
|
||
|
|
Type: "make",
|
||
|
|
Description: "make(" + typeStr + ")" + sizeStr,
|
||
|
|
InLoop: inLoop,
|
||
|
|
Position: newPosition(pos),
|
||
|
|
Context: extractContext(src, pos),
|
||
|
|
}
|
||
|
|
analysis.Allocations = append(analysis.Allocations, alloc)
|
||
|
|
|
||
|
|
if inLoop {
|
||
|
|
issue := AllocationIssue{
|
||
|
|
Type: "make_in_loop",
|
||
|
|
Description: "make() called inside loop - consider pre-allocating",
|
||
|
|
Position: newPosition(pos),
|
||
|
|
}
|
||
|
|
analysis.Issues = append(analysis.Issues, issue)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
case "new":
|
||
|
|
if len(call.Args) > 0 {
|
||
|
|
typeStr := exprToString(call.Args[0])
|
||
|
|
|
||
|
|
alloc := MemoryAllocation{
|
||
|
|
Type: "new",
|
||
|
|
Description: "new(" + typeStr + ")",
|
||
|
|
InLoop: inLoop,
|
||
|
|
Position: newPosition(pos),
|
||
|
|
Context: extractContext(src, pos),
|
||
|
|
}
|
||
|
|
analysis.Allocations = append(analysis.Allocations, alloc)
|
||
|
|
|
||
|
|
if inLoop {
|
||
|
|
issue := AllocationIssue{
|
||
|
|
Type: "new_in_loop",
|
||
|
|
Description: "new() called inside loop - consider pre-allocating",
|
||
|
|
Position: newPosition(pos),
|
||
|
|
}
|
||
|
|
analysis.Issues = append(analysis.Issues, issue)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
case "append":
|
||
|
|
alloc := MemoryAllocation{
|
||
|
|
Type: "append",
|
||
|
|
Description: "append() may cause reallocation",
|
||
|
|
InLoop: inLoop,
|
||
|
|
Position: newPosition(pos),
|
||
|
|
Context: extractContext(src, pos),
|
||
|
|
}
|
||
|
|
analysis.Allocations = append(analysis.Allocations, alloc)
|
||
|
|
|
||
|
|
if inLoop && !hasPreallocation(file, call) {
|
||
|
|
issue := AllocationIssue{
|
||
|
|
Type: "append_in_loop",
|
||
|
|
Description: "append() in loop without pre-allocation - consider pre-allocating slice",
|
||
|
|
Position: newPosition(pos),
|
||
|
|
}
|
||
|
|
analysis.Issues = append(analysis.Issues, issue)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func analyzeMethodCall(sel *ast.SelectorExpr, call *ast.CallExpr, file *ast.File, fset *token.FileSet, src []byte, analysis *AllocationAnalysis) {
|
||
|
|
// Check for common allocation patterns in method calls
|
||
|
|
methodName := sel.Sel.Name
|
||
|
|
|
||
|
|
// Check for strings.Builder inefficiencies
|
||
|
|
if methodName == "WriteString" || methodName == "Write" {
|
||
|
|
if ident, ok := sel.X.(*ast.Ident); ok {
|
||
|
|
if isStringBuilderType(file, ident) && isInLoop(file, call) {
|
||
|
|
// This is okay - strings.Builder is designed for this
|
||
|
|
return
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func analyzeStringConcat(binExpr *ast.BinaryExpr, file *ast.File, fset *token.FileSet, src []byte, analysis *AllocationAnalysis) {
|
||
|
|
// Check if this is string concatenation
|
||
|
|
if !isStringType(binExpr.X) && !isStringType(binExpr.Y) {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
pos := fset.Position(binExpr.Pos())
|
||
|
|
inLoop := isInLoop(file, binExpr)
|
||
|
|
|
||
|
|
if inLoop {
|
||
|
|
alloc := MemoryAllocation{
|
||
|
|
Type: "string_concat",
|
||
|
|
Description: "String concatenation with +",
|
||
|
|
InLoop: true,
|
||
|
|
Position: newPosition(pos),
|
||
|
|
Context: extractContext(src, pos),
|
||
|
|
}
|
||
|
|
analysis.Allocations = append(analysis.Allocations, alloc)
|
||
|
|
|
||
|
|
issue := AllocationIssue{
|
||
|
|
Type: "string_concat_in_loop",
|
||
|
|
Description: "String concatenation in loop - use strings.Builder instead",
|
||
|
|
Position: newPosition(pos),
|
||
|
|
}
|
||
|
|
analysis.Issues = append(analysis.Issues, issue)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func isStringType(expr ast.Expr) bool {
|
||
|
|
// Simple heuristic - check for string literals or string-like identifiers
|
||
|
|
switch e := expr.(type) {
|
||
|
|
case *ast.BasicLit:
|
||
|
|
return e.Kind == token.STRING
|
||
|
|
case *ast.Ident:
|
||
|
|
// This is a simplification - ideally we'd have type info
|
||
|
|
return strings.Contains(strings.ToLower(e.Name), "str") ||
|
||
|
|
strings.Contains(strings.ToLower(e.Name), "msg") ||
|
||
|
|
strings.Contains(strings.ToLower(e.Name), "text")
|
||
|
|
}
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
func isEscaping(file *ast.File, unary *ast.UnaryExpr) bool {
|
||
|
|
// Simple escape analysis - if address is assigned or passed to function
|
||
|
|
var escapes bool
|
||
|
|
|
||
|
|
ast.Inspect(file, func(n ast.Node) bool {
|
||
|
|
switch node := n.(type) {
|
||
|
|
case *ast.AssignStmt:
|
||
|
|
for _, rhs := range node.Rhs {
|
||
|
|
if rhs == unary {
|
||
|
|
escapes = true
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
case *ast.CallExpr:
|
||
|
|
for _, arg := range node.Args {
|
||
|
|
if arg == unary {
|
||
|
|
escapes = true
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
case *ast.ReturnStmt:
|
||
|
|
for _, result := range node.Results {
|
||
|
|
if result == unary {
|
||
|
|
escapes = true
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return true
|
||
|
|
})
|
||
|
|
|
||
|
|
return escapes
|
||
|
|
}
|
||
|
|
|
||
|
|
func hasPreallocation(file *ast.File, appendCall *ast.CallExpr) bool {
|
||
|
|
// Check if the slice being appended to was pre-allocated
|
||
|
|
if len(appendCall.Args) == 0 {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
// Get the slice being appended to
|
||
|
|
sliceName := extractSliceName(appendCall.Args[0])
|
||
|
|
if sliceName == "" {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
// Look for make() call with capacity
|
||
|
|
var hasCapacity bool
|
||
|
|
ast.Inspect(file, func(n ast.Node) bool {
|
||
|
|
if assign, ok := n.(*ast.AssignStmt); ok {
|
||
|
|
for i, lhs := range assign.Lhs {
|
||
|
|
if ident, ok := lhs.(*ast.Ident); ok && ident.Name == sliceName {
|
||
|
|
if i < len(assign.Rhs) {
|
||
|
|
if call, ok := assign.Rhs[i].(*ast.CallExpr); ok {
|
||
|
|
if ident, ok := call.Fun.(*ast.Ident); ok && ident.Name == "make" {
|
||
|
|
// Check if make has capacity argument
|
||
|
|
if len(call.Args) >= 3 {
|
||
|
|
hasCapacity = true
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return true
|
||
|
|
})
|
||
|
|
|
||
|
|
return hasCapacity
|
||
|
|
}
|
||
|
|
|
||
|
|
func extractSliceName(expr ast.Expr) string {
|
||
|
|
if ident, ok := expr.(*ast.Ident); ok {
|
||
|
|
return ident.Name
|
||
|
|
}
|
||
|
|
return ""
|
||
|
|
}
|
||
|
|
|
||
|
|
func isStringBuilderType(file *ast.File, ident *ast.Ident) bool {
|
||
|
|
// Check if identifier is of type strings.Builder
|
||
|
|
var isBuilder bool
|
||
|
|
|
||
|
|
ast.Inspect(file, func(n ast.Node) bool {
|
||
|
|
switch node := n.(type) {
|
||
|
|
case *ast.ValueSpec:
|
||
|
|
for i, name := range node.Names {
|
||
|
|
if name.Name == ident.Name {
|
||
|
|
if node.Type != nil {
|
||
|
|
if sel, ok := node.Type.(*ast.SelectorExpr); ok {
|
||
|
|
if pkg, ok := sel.X.(*ast.Ident); ok {
|
||
|
|
isBuilder = pkg.Name == "strings" && sel.Sel.Name == "Builder"
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} else if i < len(node.Values) {
|
||
|
|
// Check initialization
|
||
|
|
if comp, ok := node.Values[i].(*ast.CompositeLit); ok {
|
||
|
|
if sel, ok := comp.Type.(*ast.SelectorExpr); ok {
|
||
|
|
if pkg, ok := sel.X.(*ast.Ident); ok {
|
||
|
|
isBuilder = pkg.Name == "strings" && sel.Sel.Name == "Builder"
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return true
|
||
|
|
})
|
||
|
|
|
||
|
|
return isBuilder
|
||
|
|
}
|
||
|
|
|
||
|
|
func findAllocationPatterns(file *ast.File, fset *token.FileSet, src []byte, analysis *AllocationAnalysis) {
|
||
|
|
// Look for interface{} allocations
|
||
|
|
ast.Inspect(file, func(n ast.Node) bool {
|
||
|
|
if callExpr, ok := n.(*ast.CallExpr); ok {
|
||
|
|
// Check for fmt.Sprintf and similar
|
||
|
|
if sel, ok := callExpr.Fun.(*ast.SelectorExpr); ok {
|
||
|
|
if ident, ok := sel.X.(*ast.Ident); ok && ident.Name == "fmt" {
|
||
|
|
if strings.HasPrefix(sel.Sel.Name, "Sprint") {
|
||
|
|
pos := fset.Position(callExpr.Pos())
|
||
|
|
alloc := MemoryAllocation{
|
||
|
|
Type: "fmt_sprintf",
|
||
|
|
Description: "fmt." + sel.Sel.Name + " allocates for interface{} conversions",
|
||
|
|
InLoop: isInLoop(file, callExpr),
|
||
|
|
Position: newPosition(pos),
|
||
|
|
Context: extractContext(src, pos),
|
||
|
|
}
|
||
|
|
analysis.Allocations = append(analysis.Allocations, alloc)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return true
|
||
|
|
})
|
||
|
|
}
|