Skip to content

Instantly share code, notes, and snippets.

@iljavs
Created August 24, 2025 17:13
Show Gist options
  • Select an option

  • Save iljavs/cf76a84f64108234e4782d4584f28dd2 to your computer and use it in GitHub Desktop.

Select an option

Save iljavs/cf76a84f64108234e4782d4584f28dd2 to your computer and use it in GitHub Desktop.
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