Files
gocp/tool_find_errors.go
2025-06-27 21:22:36 -07:00

102 lines
2.9 KiB
Go

package main
import (
"go/ast"
"go/token"
"strings"
)
// Error handling types
type ErrorInfo struct {
File string `json:"file"`
UnhandledErrors []ErrorContext `json:"unhandled_errors,omitempty"`
ErrorChecks []ErrorContext `json:"error_checks,omitempty"`
ErrorReturns []ErrorContext `json:"error_returns,omitempty"`
}
type ErrorContext struct {
Context string `json:"context"`
Type string `json:"type"`
Position Position `json:"position"`
}
func findErrors(dir string) ([]ErrorInfo, error) {
var errors []ErrorInfo
err := walkGoFiles(dir, func(path string, src []byte, file *ast.File, fset *token.FileSet) error {
info := ErrorInfo{
File: path,
}
ast.Inspect(file, func(n ast.Node) bool {
switch x := n.(type) {
// Find function calls that return errors but aren't checked
case *ast.ExprStmt:
if call, ok := x.X.(*ast.CallExpr); ok {
// Check if this function likely returns an error
if returnsError(call, file) {
pos := fset.Position(call.Pos())
context := extractContext(src, pos)
info.UnhandledErrors = append(info.UnhandledErrors, ErrorContext{
Context: context,
Type: "unchecked_call",
Position: newPosition(pos),
})
}
}
// Find error checks
case *ast.IfStmt:
if isErrorCheck(x) {
pos := fset.Position(x.Pos())
context := extractContext(src, pos)
info.ErrorChecks = append(info.ErrorChecks, ErrorContext{
Context: context,
Type: "error_check",
Position: newPosition(pos),
})
}
// Find error returns
case *ast.ReturnStmt:
for _, result := range x.Results {
if ident, ok := result.(*ast.Ident); ok && (ident.Name == "err" || strings.Contains(ident.Name, "error")) {
pos := fset.Position(x.Pos())
context := extractContext(src, pos)
info.ErrorReturns = append(info.ErrorReturns, ErrorContext{
Context: context,
Type: "error_return",
Position: newPosition(pos),
})
break
}
}
}
return true
})
if len(info.UnhandledErrors) > 0 || len(info.ErrorChecks) > 0 || len(info.ErrorReturns) > 0 {
errors = append(errors, info)
}
return nil
})
return errors, err
}
func returnsError(call *ast.CallExpr, file *ast.File) bool {
// Simple heuristic: check if the function name suggests it returns an error
switch fun := call.Fun.(type) {
case *ast.Ident:
name := fun.Name
return strings.HasPrefix(name, "New") || strings.HasPrefix(name, "Create") ||
strings.HasPrefix(name, "Open") || strings.HasPrefix(name, "Read") ||
strings.HasPrefix(name, "Write") || strings.HasPrefix(name, "Parse")
case *ast.SelectorExpr:
name := fun.Sel.Name
return strings.HasPrefix(name, "New") || strings.HasPrefix(name, "Create") ||
strings.HasPrefix(name, "Open") || strings.HasPrefix(name, "Read") ||
strings.HasPrefix(name, "Write") || strings.HasPrefix(name, "Parse")
}
return false
}