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

279 lines
7.5 KiB
Go

package main
import (
"go/ast"
"go/token"
)
type ReflectionUsage struct {
Type string `json:"type"` // "TypeOf", "ValueOf", "MethodByName", etc.
Target string `json:"target"`
Position Position `json:"position"`
Context string `json:"context"`
}
type ReflectionAnalysis struct {
Usages []ReflectionUsage `json:"usages"`
Issues []ReflectionIssue `json:"issues"`
}
type ReflectionIssue struct {
Type string `json:"type"`
Description string `json:"description"`
Position Position `json:"position"`
}
func findReflectionUsage(dir string) (*ReflectionAnalysis, error) {
analysis := &ReflectionAnalysis{
Usages: []ReflectionUsage{},
Issues: []ReflectionIssue{},
}
err := walkGoFiles(dir, func(path string, src []byte, file *ast.File, fset *token.FileSet) error {
// Check if reflect package is imported
hasReflectImport := false
for _, imp := range file.Imports {
if imp.Path != nil && imp.Path.Value == `"reflect"` {
hasReflectImport = true
break
}
}
if !hasReflectImport {
return nil
}
ast.Inspect(file, func(n ast.Node) bool {
if callExpr, ok := n.(*ast.CallExpr); ok {
analyzeReflectCall(callExpr, file, fset, src, analysis)
}
return true
})
return nil
})
return analysis, err
}
func analyzeReflectCall(call *ast.CallExpr, file *ast.File, fset *token.FileSet, src []byte, analysis *ReflectionAnalysis) {
sel, ok := call.Fun.(*ast.SelectorExpr)
if !ok {
return
}
// Check if it's a reflect package call
ident, ok := sel.X.(*ast.Ident)
if !ok || ident.Name != "reflect" {
return
}
pos := fset.Position(call.Pos())
methodName := sel.Sel.Name
target := ""
if len(call.Args) > 0 {
target = exprToString(call.Args[0])
}
usage := ReflectionUsage{
Type: methodName,
Target: target,
Position: newPosition(pos),
Context: extractContext(src, pos),
}
analysis.Usages = append(analysis.Usages, usage)
// Analyze specific reflection patterns
switch methodName {
case "TypeOf", "ValueOf":
if isInLoop(file, call) {
issue := ReflectionIssue{
Type: "reflection_in_loop",
Description: "reflect." + methodName + " called in loop - consider caching result",
Position: newPosition(pos),
}
analysis.Issues = append(analysis.Issues, issue)
}
case "MethodByName", "FieldByName":
// These are particularly slow
issue := ReflectionIssue{
Type: "slow_reflection",
Description: "reflect." + methodName + " is slow - consider caching or avoiding if possible",
Position: newPosition(pos),
}
analysis.Issues = append(analysis.Issues, issue)
if isInLoop(file, call) {
issue := ReflectionIssue{
Type: "slow_reflection_in_loop",
Description: "reflect." + methodName + " in loop is very inefficient",
Position: newPosition(pos),
}
analysis.Issues = append(analysis.Issues, issue)
}
case "DeepEqual":
if isInHotPath(file, call) {
issue := ReflectionIssue{
Type: "deep_equal_performance",
Description: "reflect.DeepEqual is expensive - consider custom comparison for hot paths",
Position: newPosition(pos),
}
analysis.Issues = append(analysis.Issues, issue)
}
case "Copy", "AppendSlice", "MakeSlice", "MakeMap", "MakeChan":
// These allocate memory
if isInLoop(file, call) {
issue := ReflectionIssue{
Type: "reflect_allocation_in_loop",
Description: "reflect." + methodName + " allocates memory in loop",
Position: newPosition(pos),
}
analysis.Issues = append(analysis.Issues, issue)
}
}
// Check for unsafe reflection patterns
checkUnsafeReflection(call, file, fset, analysis)
}
func checkUnsafeReflection(call *ast.CallExpr, file *ast.File, fset *token.FileSet, analysis *ReflectionAnalysis) {
// Look for patterns like Value.Interface() without type checking
ast.Inspect(file, func(n ast.Node) bool {
if selExpr, ok := n.(*ast.SelectorExpr); ok {
if selExpr.Sel.Name == "Interface" {
// Check if this is on a reflect.Value
if isReflectValueExpr(file, selExpr.X) {
// Check if result is used in type assertion without ok check
if parent := findParentNode(file, selExpr); parent != nil {
if typeAssert, ok := parent.(*ast.TypeAssertExpr); ok {
if !isUsedWithOkCheck(file, typeAssert) {
pos := fset.Position(typeAssert.Pos())
issue := ReflectionIssue{
Type: "unsafe_interface_conversion",
Description: "Type assertion on reflect.Value.Interface() without ok check",
Position: newPosition(pos),
}
analysis.Issues = append(analysis.Issues, issue)
}
}
}
}
}
}
return true
})
}
func isReflectValueExpr(file *ast.File, expr ast.Expr) bool {
// Simple heuristic - check if expression contains "reflect.Value" operations
switch e := expr.(type) {
case *ast.CallExpr:
if sel, ok := e.Fun.(*ast.SelectorExpr); ok {
if ident, ok := sel.X.(*ast.Ident); ok && ident.Name == "reflect" {
return sel.Sel.Name == "ValueOf" || sel.Sel.Name == "Indirect"
}
}
case *ast.Ident:
// Check if it's a variable of type reflect.Value
return isReflectValueVar(file, e.Name)
}
return false
}
func isReflectValueVar(file *ast.File, varName string) bool {
var isValue 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 == varName {
if node.Type != nil {
if sel, ok := node.Type.(*ast.SelectorExpr); ok {
if ident, ok := sel.X.(*ast.Ident); ok {
isValue = ident.Name == "reflect" && sel.Sel.Name == "Value"
return false
}
}
} else if i < len(node.Values) {
// Check if assigned from reflect.ValueOf
if call, ok := node.Values[i].(*ast.CallExpr); ok {
if sel, ok := call.Fun.(*ast.SelectorExpr); ok {
if ident, ok := sel.X.(*ast.Ident); ok {
isValue = ident.Name == "reflect" && sel.Sel.Name == "ValueOf"
return false
}
}
}
}
}
}
}
return true
})
return isValue
}
func findParentNode(file *ast.File, target ast.Node) ast.Node {
var parent ast.Node
ast.Inspect(file, func(n ast.Node) bool {
// This is a simplified parent finder
switch node := n.(type) {
case *ast.TypeAssertExpr:
if node.X == target {
parent = node
return false
}
case *ast.CallExpr:
for _, arg := range node.Args {
if arg == target {
parent = node
return false
}
}
}
return true
})
return parent
}
func isInHotPath(file *ast.File, node ast.Node) bool {
// Check if node is in a function that looks like a hot path
var inHotPath bool
ast.Inspect(file, func(n ast.Node) bool {
if fn, ok := n.(*ast.FuncDecl); ok && containsNode(fn, node) {
// Check function name for common hot path patterns
name := fn.Name.Name
if name == "ServeHTTP" || name == "Handle" || name == "Process" ||
name == "Execute" || name == "Run" || name == "Do" {
inHotPath = true
return false
}
// Check if function is called frequently (in loops)
if isFunctionCalledInLoop(file, fn.Name.Name) {
inHotPath = true
return false
}
}
return true
})
return inHotPath
}
func isFunctionCalledInLoop(file *ast.File, funcName string) bool {
var calledInLoop bool
ast.Inspect(file, func(n ast.Node) bool {
if call, ok := n.(*ast.CallExpr); ok {
if ident, ok := call.Fun.(*ast.Ident); ok && ident.Name == funcName {
if isInLoop(file, call) {
calledInLoop = true
return false
}
}
}
return true
})
return calledInLoop
}