| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256 | //go:generate go run .package mainimport (	"fmt"	"go/ast"	"go/parser"	"go/token"	"os"	"path/filepath"	"runtime"	"strings"	"github.com/uozi-tech/cosy/logger")// Structure for notification function callstype NotificationCall struct {	Type    string	Title   string	Content string	Path    string}// Directories to excludevar excludeDirs = []string{	".devcontainer", ".github", ".idea", ".pnpm-store",	".vscode", "app", "query", "tmp", "cmd",}// Main functionfunc main() {	logger.Init("release")	// Start scanning from the project root	_, file, _, ok := runtime.Caller(0)	if !ok {		logger.Error("Unable to get the current file")		return	}	root := filepath.Join(filepath.Dir(file), "../../")	calls := []NotificationCall{}	// Scan all Go files	err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {		if err != nil {			return err		}		// Skip excluded directories		for _, dir := range excludeDirs {			if strings.Contains(path, dir) {				if info.IsDir() {					return filepath.SkipDir				}				return nil			}		}		// Only process Go files		if !info.IsDir() && strings.HasSuffix(path, ".go") {			findNotificationCalls(path, &calls)		}		return nil	})	if err != nil {		logger.Errorf("Error walking the path: %v\n", err)		return	}	// Generate a single TS file	generateSingleTSFile(root, calls)	logger.Infof("Found %d notification calls\n", len(calls))}// Find notification function calls in Go filesfunc findNotificationCalls(filePath string, calls *[]NotificationCall) {	// Parse Go code	fset := token.NewFileSet()	node, err := parser.ParseFile(fset, filePath, nil, parser.ParseComments)	if err != nil {		logger.Errorf("Error parsing %s: %v\n", filePath, err)		return	}	// Traverse the AST to find function calls	ast.Inspect(node, func(n ast.Node) bool {		callExpr, ok := n.(*ast.CallExpr)		if !ok {			return true		}		// Check if it's a call to the notification package		selExpr, ok := callExpr.Fun.(*ast.SelectorExpr)		if !ok {			return true		}		xident, ok := selExpr.X.(*ast.Ident)		if !ok {			return true		}		// Check if it's one of the functions we're interested in: notification.Info/Error/Warning/Success		if xident.Name == "notification" {			funcName := selExpr.Sel.Name			if funcName == "Info" || funcName == "Error" || funcName == "Warning" || funcName == "Success" {				// Function must have at least two parameters (title, content)				if len(callExpr.Args) >= 2 {					titleArg := callExpr.Args[0]					contentArg := callExpr.Args[1]					// Get parameter values					title := getStringValue(titleArg)					content := getStringValue(contentArg)					// Ignore cases where content is a variable name or function call					if content != "" && !isVariableOrFunctionCall(content) {						*calls = append(*calls, NotificationCall{							Type:    funcName,							Title:   title,							Content: content,							Path:    filePath,						})					}				}			}		}		return true	})}// Check if the string is a variable name or function callfunc isVariableOrFunctionCall(s string) bool {	// Simple check: if the string doesn't contain spaces or quotes, it might be a variable name	if !strings.Contains(s, " ") && !strings.Contains(s, "\"") && !strings.Contains(s, "'") {		return true	}	// If it looks like a function call, e.g., err.Error()	if strings.Contains(s, "(") && strings.Contains(s, ")") {		return true	}	return false}// Get string value from AST nodefunc getStringValue(expr ast.Expr) string {	// Direct string	if lit, ok := expr.(*ast.BasicLit); ok && lit.Kind == token.STRING {		// Return string without quotes		return strings.Trim(lit.Value, "\"")	}	// Recover string value from source code expression	var str strings.Builder	if bin, ok := expr.(*ast.BinaryExpr); ok {		// Handle string concatenation expression		leftStr := getStringValue(bin.X)		rightStr := getStringValue(bin.Y)		str.WriteString(leftStr)		str.WriteString(rightStr)	}	if str.Len() > 0 {		return str.String()	}	// Return empty string if unable to parse as string	return ""}// Generate a single TypeScript filefunc generateSingleTSFile(root string, calls []NotificationCall) {	// Create target directory	targetDir := filepath.Join(root, "app/src/components/Notification")	err := os.MkdirAll(targetDir, 0755)	if err != nil {		logger.Errorf("Error creating directory %s: %v\n", targetDir, err)		return	}	// Create file name	tsFilePath := filepath.Join(targetDir, "notifications.ts")	// Prepare file content	var content strings.Builder	content.WriteString("// Auto-generated notification texts\n")	content.WriteString("// Extracted from Go source code notification function calls\n")	content.WriteString("/* eslint-disable ts/no-explicit-any */\n\n")	content.WriteString("const notifications: Record<string, { title: () => string, content: (args: any) => string }> = {\n")	// Track used keys to avoid duplicates	usedKeys := make(map[string]bool)	// Organize notifications by directory	messagesByDir := make(map[string][]NotificationCall)	for _, call := range calls {		dir := filepath.Dir(call.Path)		// Extract module name from directory path		dirParts := strings.Split(dir, "/")		moduleName := dirParts[len(dirParts)-1]		if strings.HasPrefix(dir, "internal/") || strings.HasPrefix(dir, "api/") {			messagesByDir[moduleName] = append(messagesByDir[moduleName], call)		} else {			messagesByDir["general"] = append(messagesByDir["general"], call)		}	}	// Add comments for each module and write notifications	for module, moduleCalls := range messagesByDir {		content.WriteString(fmt.Sprintf("\n  // %s module notifications\n", module))		for _, call := range moduleCalls {			// Escape quotes in title and content			escapedTitle := strings.ReplaceAll(call.Title, "'", "\\'")			escapedContent := strings.ReplaceAll(call.Content, "'", "\\'")			// Use just the title as the key			key := call.Title			// Check if key is already used, generate unique key if necessary			uniqueKey := key			counter := 1			for usedKeys[uniqueKey] {				uniqueKey = fmt.Sprintf("%s_%d", key, counter)				counter++			}			usedKeys[uniqueKey] = true			// Write record with both title and content as functions			content.WriteString(fmt.Sprintf("  '%s': {\n", uniqueKey))			content.WriteString(fmt.Sprintf("    title: () => $gettext('%s'),\n", escapedTitle))			content.WriteString(fmt.Sprintf("    content: (args: any) => $gettext('%s', args),\n", escapedContent))			content.WriteString("  },\n")		}	}	content.WriteString("}\n\n")	content.WriteString("export default notifications\n")	// Write file	err = os.WriteFile(tsFilePath, []byte(content.String()), 0644)	if err != nil {		logger.Errorf("Error writing TS file %s: %v\n", tsFilePath, err)		return	}	logger.Infof("Generated single TS file: %s with %d notifications\n", tsFilePath, len(calls))}
 |