https://gist.github.com/podhmo/10efd01be20479325485c075451fd546
Last active
September 20, 2025 12:51
-
-
Save podhmo/63692370e15a7275847f2b0ac36dfb94 to your computer and use it in GitHub Desktop.
goscanのScannerのcallgraphを作りたいと言ったらcopilotがなんか生成してきた
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 ( | |
"context" | |
"fmt" | |
"go/ast" | |
"go/parser" | |
"go/token" | |
"log" | |
"os" | |
"path/filepath" | |
"sort" | |
"strings" | |
) | |
// MethodCall represents a method call from one method to another | |
type MethodCall struct { | |
From string // Method that makes the call | |
To string // Method that is called | |
} | |
// MethodInfo holds information about a method | |
type MethodInfo struct { | |
Name string // Method name | |
Receiver string // Receiver type (e.g., "Scanner") | |
Package string // Package name (e.g., "goscan" or "scanner") | |
Calls []string // Methods called from this method | |
} | |
// CallGraph represents the entire call graph | |
type CallGraph struct { | |
Methods map[string]*MethodInfo // key: package.receiver.method | |
Calls []MethodCall | |
} | |
func main() { | |
if len(os.Args) < 2 { | |
log.Fatal("Usage: go run main.go <root-dir>") | |
} | |
rootDir := os.Args[1] | |
ctx := context.Background() | |
callGraph, err := analyzeCallGraph(ctx, rootDir) | |
if err != nil { | |
log.Fatalf("Error analyzing call graph: %v", err) | |
} | |
// Output results | |
fmt.Println("# Scanner Methods Call Graph") | |
fmt.Println() | |
// List all Scanner methods | |
fmt.Println("## All Scanner Methods") | |
fmt.Println() | |
var allMethods []string | |
for _, method := range callGraph.Methods { | |
if method.Receiver == "Scanner" { | |
allMethods = append(allMethods, fmt.Sprintf("- %s.%s.%s", method.Package, method.Receiver, method.Name)) | |
} | |
} | |
sort.Strings(allMethods) | |
for _, method := range allMethods { | |
fmt.Println(method) | |
} | |
fmt.Println() | |
// Output call relationships | |
fmt.Println("## Method Call Relationships") | |
fmt.Println() | |
// Group calls by the calling method | |
callMap := make(map[string][]string) | |
for _, call := range callGraph.Calls { | |
callMap[call.From] = append(callMap[call.From], call.To) | |
} | |
var callers []string | |
for caller := range callMap { | |
callers = append(callers, caller) | |
} | |
sort.Strings(callers) | |
for _, caller := range callers { | |
targets := callMap[caller] | |
sort.Strings(targets) | |
fmt.Printf("### %s\n", caller) | |
fmt.Println("Calls:") | |
for _, target := range targets { | |
fmt.Printf("- %s\n", target) | |
} | |
fmt.Println() | |
} | |
// Output in Mermaid format | |
fmt.Println("## Mermaid Diagram") | |
fmt.Println() | |
fmt.Println("```mermaid") | |
fmt.Println("graph TD") | |
// Create node definitions | |
nodeMap := make(map[string]string) | |
for key, method := range callGraph.Methods { | |
if method.Receiver == "Scanner" { | |
nodeId := strings.ReplaceAll(key, ".", "_") | |
nodeMap[key] = nodeId | |
fmt.Printf(" %s[\"%s.%s\"]\n", nodeId, method.Package, method.Name) | |
} | |
} | |
// Create edges | |
for _, call := range callGraph.Calls { | |
fromNode, fromExists := nodeMap[call.From] | |
toNode, toExists := nodeMap[call.To] | |
if fromExists && toExists { | |
fmt.Printf(" %s --> %s\n", fromNode, toNode) | |
} | |
} | |
fmt.Println("```") | |
} | |
func analyzeCallGraph(ctx context.Context, rootDir string) (*CallGraph, error) { | |
fset := token.NewFileSet() | |
// Parse relevant Go files | |
files := []string{ | |
filepath.Join(rootDir, "goscan.go"), | |
filepath.Join(rootDir, "scanner", "scanner.go"), | |
} | |
callGraph := &CallGraph{ | |
Methods: make(map[string]*MethodInfo), | |
Calls: []MethodCall{}, | |
} | |
for _, filePath := range files { | |
if err := analyzeFile(fset, filePath, callGraph); err != nil { | |
return nil, fmt.Errorf("error analyzing file %s: %w", filePath, err) | |
} | |
} | |
return callGraph, nil | |
} | |
func analyzeFile(fset *token.FileSet, filePath string, callGraph *CallGraph) error { | |
src, err := os.ReadFile(filePath) | |
if err != nil { | |
return fmt.Errorf("reading file: %w", err) | |
} | |
// Parse the file | |
file, err := parser.ParseFile(fset, filePath, src, parser.ParseComments) | |
if err != nil { | |
return fmt.Errorf("parsing file: %w", err) | |
} | |
packageName := file.Name.Name | |
// Walk the AST to find method definitions and calls | |
ast.Inspect(file, func(n ast.Node) bool { | |
switch node := n.(type) { | |
case *ast.FuncDecl: | |
if node.Recv != nil && len(node.Recv.List) > 0 { | |
// This is a method | |
methodName := node.Name.Name | |
receiverType := extractReceiverType(node.Recv.List[0].Type) | |
if receiverType == "Scanner" { | |
methodKey := fmt.Sprintf("%s.%s.%s", packageName, receiverType, methodName) | |
method := &MethodInfo{ | |
Name: methodName, | |
Receiver: receiverType, | |
Package: packageName, | |
Calls: []string{}, | |
} | |
// Find method calls within this method | |
ast.Inspect(node, func(inner ast.Node) bool { | |
if callExpr, ok := inner.(*ast.CallExpr); ok { | |
if targetMethod := extractMethodCall(callExpr, packageName); targetMethod != "" { | |
method.Calls = append(method.Calls, targetMethod) | |
// Add to call graph | |
callGraph.Calls = append(callGraph.Calls, MethodCall{ | |
From: methodKey, | |
To: targetMethod, | |
}) | |
} | |
} | |
return true | |
}) | |
callGraph.Methods[methodKey] = method | |
} | |
} | |
} | |
return true | |
}) | |
return nil | |
} | |
func extractReceiverType(expr ast.Expr) string { | |
switch t := expr.(type) { | |
case *ast.StarExpr: | |
return extractReceiverType(t.X) | |
case *ast.Ident: | |
return t.Name | |
default: | |
return "" | |
} | |
} | |
func extractMethodCall(callExpr *ast.CallExpr, currentPackage string) string { | |
switch fun := callExpr.Fun.(type) { | |
case *ast.SelectorExpr: | |
// Check if it's a method call on 's' (the scanner receiver) | |
if ident, ok := fun.X.(*ast.Ident); ok { | |
if ident.Name == "s" { | |
// This is a call to another Scanner method | |
return fmt.Sprintf("%s.Scanner.%s", currentPackage, fun.Sel.Name) | |
} | |
// Check for calls to s.scanner (internal scanner) | |
if ident.Name == "s" { | |
// Look for s.scanner.Method() pattern by checking if the selector is on a field | |
return "" | |
} | |
} | |
// Check for s.scanner.Method() calls | |
if selectorExpr, ok := fun.X.(*ast.SelectorExpr); ok { | |
if ident, ok := selectorExpr.X.(*ast.Ident); ok && ident.Name == "s" { | |
if selectorExpr.Sel.Name == "scanner" { | |
// This is s.scanner.Method() | |
return fmt.Sprintf("scanner.Scanner.%s", fun.Sel.Name) | |
} | |
} | |
} | |
} | |
return "" | |
} |
Author
podhmo
commented
Sep 20, 2025
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment