279 lines
7.5 KiB
Go
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
|
|
} |