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