Created
August 24, 2025 17:13
-
-
Save iljavs/cf76a84f64108234e4782d4584f28dd2 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package main | |
| import ( | |
| "fmt" | |
| "go/ast" | |
| "go/parser" | |
| "go/token" | |
| "log" | |
| "os" | |
| "path/filepath" | |
| "strings" | |
| ) | |
| type Issue struct { | |
| Position token.Position | |
| Function string | |
| Message string | |
| ReturnStmt string | |
| NamedParams []string | |
| Assigned []string | |
| Missing []string | |
| } | |
| type Analyzer struct { | |
| fileSet *token.FileSet | |
| issues []Issue | |
| debug bool | |
| } | |
| func NewAnalyzer() *Analyzer { | |
| return &Analyzer{ | |
| fileSet: token.NewFileSet(), | |
| issues: make([]Issue, 0), | |
| debug: false, | |
| } | |
| } | |
| func (a *Analyzer) SetDebug(debug bool) { | |
| a.debug = debug | |
| } | |
| func (a *Analyzer) AnalyzeFile(filename string) error { | |
| src, err := os.ReadFile(filename) | |
| if err != nil { | |
| return fmt.Errorf("failed to read file %s: %w", filename, err) | |
| } | |
| node, err := parser.ParseFile( | |
| a.fileSet, | |
| filename, | |
| src, | |
| parser.ParseComments) | |
| if err != nil { | |
| return fmt.Errorf("failed to parse file %s: %w", filename, err) | |
| } | |
| ast.Inspect(node, a.inspectNode) | |
| return nil | |
| } | |
| func (a *Analyzer) inspectNode(n ast.Node) bool { | |
| switch node := n.(type) { | |
| case *ast.FuncDecl: | |
| a.analyzeFunction(node) | |
| } | |
| return true | |
| } | |
| func (a *Analyzer) analyzeFunction(fn *ast.FuncDecl) { | |
| if fn.Type.Results == nil { | |
| return | |
| } | |
| namedReturns := a.extractNamedReturns(fn.Type.Results) | |
| if len(namedReturns) == 0 { | |
| return | |
| } | |
| funcName := fn.Name.Name | |
| if fn.Recv != nil { | |
| recvType := a.getReceiverType(fn.Recv) | |
| funcName = fmt.Sprintf("(%s).%s", recvType, fn.Name.Name) | |
| } | |
| if a.debug { | |
| fmt.Printf("\n=== Analyzing function: %s ===\n", funcName) | |
| fmt.Printf("Named returns: %v\n", namedReturns) | |
| } | |
| if fn.Body != nil { | |
| a.analyzeReturns(fn.Body, funcName, namedReturns) | |
| } | |
| } | |
| func (a *Analyzer) extractNamedReturns(results *ast.FieldList) []string { | |
| var named []string | |
| for _, field := range results.List { | |
| for _, name := range field.Names { | |
| if name.Name != "" && name.Name != "_" { | |
| named = append(named, name.Name) | |
| } | |
| } | |
| } | |
| return named | |
| } | |
| func (a *Analyzer) getReceiverType(recv *ast.FieldList) string { | |
| if len(recv.List) == 0 { | |
| return "unknown" | |
| } | |
| field := recv.List[0] | |
| switch t := field.Type.(type) { | |
| case *ast.Ident: | |
| return t.Name | |
| case *ast.StarExpr: | |
| if ident, ok := t.X.(*ast.Ident); ok { | |
| return "*" + ident.Name | |
| } | |
| } | |
| return "unknown" | |
| } | |
| func (a *Analyzer) analyzeReturns( | |
| block ast.Node, | |
| funcName string, | |
| namedReturns []string) { | |
| assignments := a.collectAssignments(block, namedReturns) | |
| if a.debug { | |
| fmt.Printf("Function %s: Found %d assignments\n", | |
| funcName, | |
| len(assignments)) | |
| } | |
| ast.Inspect(block, func(n ast.Node) bool { | |
| switch node := n.(type) { | |
| case *ast.ReturnStmt: | |
| if a.debug { | |
| fmt.Printf("Found return statement in %s\n", funcName) | |
| } | |
| a.checkReturnStatement(node, funcName, namedReturns, assignments) | |
| case *ast.FuncDecl, *ast.FuncLit: | |
| // Don't recurse into nested functions | |
| return false | |
| } | |
| return true | |
| }) | |
| } | |
| type AssignmentInfo struct { | |
| VarName string | |
| Pos token.Pos | |
| } | |
| func (a *Analyzer) collectAssignments( | |
| block ast.Node, | |
| namedReturns []string) []AssignmentInfo { | |
| var assignments []AssignmentInfo | |
| if a.debug { | |
| fmt.Printf("Looking for assignments to: %v\n", namedReturns) | |
| } | |
| ast.Inspect(block, func(n ast.Node) bool { | |
| switch node := n.(type) { | |
| case *ast.AssignStmt: | |
| if a.debug { | |
| fmt.Printf("Found AssignStmt with %d LHS expressions\n", | |
| len(node.Lhs)) | |
| } | |
| for i, lhs := range node.Lhs { | |
| if ident, ok := lhs.(*ast.Ident); ok { | |
| if a.debug { | |
| fmt.Printf(" LHS[%d]: %s\n", i, ident.Name) | |
| } | |
| for _, namedRet := range namedReturns { | |
| if ident.Name == namedRet { | |
| assignments = append(assignments, AssignmentInfo{ | |
| VarName: ident.Name, | |
| Pos: ident.Pos(), | |
| }) | |
| if a.debug { | |
| fmt.Printf(" -> Found assignment to %s\n", | |
| ident.Name) | |
| } | |
| } | |
| } | |
| } else if a.debug { | |
| fmt.Printf(" LHS[%d]: not an identifier (%T)\n", i, lhs) | |
| } | |
| } | |
| case *ast.IncDecStmt: | |
| // Handle increment/decrement operations (result++, err--) | |
| if ident, ok := node.X.(*ast.Ident); ok { | |
| for _, namedRet := range namedReturns { | |
| if ident.Name == namedRet { | |
| assignments = append(assignments, AssignmentInfo{ | |
| VarName: ident.Name, | |
| Pos: ident.Pos(), | |
| }) | |
| if a.debug { | |
| fmt.Printf("Found inc/dec assignment to %s\n", | |
| ident.Name) | |
| } | |
| } | |
| } | |
| } | |
| case *ast.FuncDecl, *ast.FuncLit: | |
| return false | |
| } | |
| return true | |
| }) | |
| if a.debug { | |
| fmt.Printf("Total assignments found: %d\n", len(assignments)) | |
| for _, assign := range assignments { | |
| fmt.Printf(" %s at pos %d\n", assign.VarName, assign.Pos) | |
| } | |
| } | |
| return assignments | |
| } | |
| func (a *Analyzer) checkReturnStatement( | |
| ret *ast.ReturnStmt, | |
| funcName string, | |
| namedReturns []string, | |
| assignments []AssignmentInfo) { | |
| if len(ret.Results) > 0 { | |
| if a.debug { | |
| fmt.Printf(" Return with explicit values - skipping\n") | |
| } | |
| return | |
| } | |
| if a.debug { | |
| fmt.Printf(" Checking bare return at position %d\n", ret.Pos()) | |
| } | |
| assigned := a.findAssignmentsBeforeReturn(ret, namedReturns, assignments) | |
| if a.debug { | |
| fmt.Printf(" Assigned before return: %v\n", assigned) | |
| } | |
| var missing []string | |
| for _, name := range namedReturns { | |
| found := false | |
| for _, assignedName := range assigned { | |
| if assignedName == name { | |
| found = true | |
| break | |
| } | |
| } | |
| if !found { | |
| missing = append(missing, name) | |
| } | |
| } | |
| if a.debug { | |
| fmt.Printf(" Missing assignments: %v\n", missing) | |
| } | |
| if len(missing) > 0 { | |
| pos := a.fileSet.Position(ret.Pos()) | |
| issue := Issue{ | |
| Position: pos, | |
| Function: funcName, | |
| Message: "Bare return without assigning all named return values", | |
| ReturnStmt: "return", | |
| NamedParams: namedReturns, | |
| Assigned: assigned, | |
| Missing: missing, | |
| } | |
| a.issues = append(a.issues, issue) | |
| } | |
| } | |
| func (a *Analyzer) findAssignmentsBeforeReturn( | |
| ret *ast.ReturnStmt, | |
| namedReturns []string, | |
| assignments []AssignmentInfo) []string { | |
| var assigned []string | |
| returnPos := ret.Pos() | |
| if a.debug { | |
| fmt.Printf(" Return position: %d\n", returnPos) | |
| fmt.Printf(" Available assignments: %d\n", len(assignments)) | |
| } | |
| for _, namedRet := range namedReturns { | |
| for _, assignment := range assignments { | |
| if a.debug { | |
| fmt.Printf(" Checking assignment %s at pos %d\n", | |
| assignment.VarName, | |
| assignment.Pos) | |
| } | |
| if assignment.VarName == namedRet && assignment.Pos < returnPos { | |
| found := false | |
| for _, alreadyAssigned := range assigned { | |
| if alreadyAssigned == namedRet { | |
| found = true | |
| break | |
| } | |
| } | |
| if !found { | |
| assigned = append(assigned, namedRet) | |
| if a.debug { | |
| fmt.Printf(" -> Added %s to assigned list\n", | |
| namedRet) | |
| } | |
| } | |
| break | |
| } | |
| } | |
| } | |
| return assigned | |
| } | |
| func (a *Analyzer) AnalyzeDirectory(dir string) error { | |
| return filepath.Walk(dir, | |
| func(path string, info os.FileInfo, err error) error { | |
| if err != nil { | |
| return err | |
| } | |
| if strings.HasSuffix(path, ".go") && | |
| !strings.HasSuffix(path, "_test.go") { | |
| if err := a.AnalyzeFile(path); err != nil { | |
| log.Printf("Error analyzing file %s: %v", path, err) | |
| } | |
| } | |
| return nil | |
| }) | |
| } | |
| func (a *Analyzer) PrintResults() { | |
| if len(a.issues) == 0 { | |
| fmt.Println("No issues found with named return values.") | |
| return | |
| } | |
| fmt.Printf("Found %d potential issues with named return values:\n\n", | |
| len(a.issues)) | |
| for i, issue := range a.issues { | |
| fmt.Printf("Issue #%d:\n", i+1) | |
| fmt.Printf(" File: %s:%d:%d\n", | |
| issue.Position.Filename, | |
| issue.Position.Line, | |
| issue.Position.Column) | |
| fmt.Printf(" Function: %s\n", issue.Function) | |
| fmt.Printf(" Problem: %s\n", issue.Message) | |
| fmt.Printf(" Named returns: %s\n", | |
| strings.Join(issue.NamedParams, ", ")) | |
| if len(issue.Assigned) > 0 { | |
| fmt.Printf(" Assigned: %s\n", strings.Join(issue.Assigned, ", ")) | |
| } | |
| if len(issue.Missing) > 0 { | |
| fmt.Printf(" Missing assignments: %s\n", | |
| strings.Join(issue.Missing, ", ")) | |
| } | |
| fmt.Println() | |
| } | |
| } | |
| func main() { | |
| if len(os.Args) < 2 { | |
| fmt.Println("Usage: analyzer <file_or_directory>") | |
| fmt.Println(" analyzer test (run built-in test cases)") | |
| fmt.Println("Examples:") | |
| fmt.Println(" analyzer main.go") | |
| fmt.Println(" analyzer ./src") | |
| fmt.Println(" analyzer test") | |
| os.Exit(1) | |
| } | |
| target := os.Args[1] | |
| analyzer := NewAnalyzer() | |
| analyzer.SetDebug(true) | |
| info, err := os.Stat(target) | |
| if err != nil { | |
| log.Fatalf("Error accessing %s: %v", target, err) | |
| } | |
| if info.IsDir() { | |
| err = analyzer.AnalyzeDirectory(target) | |
| } else { | |
| err = analyzer.AnalyzeFile(target) | |
| } | |
| if err != nil { | |
| log.Fatalf("Analysis failed: %v", err) | |
| } | |
| analyzer.PrintResults() | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment