generate.go 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. //go:generate go run .
  2. package main
  3. import (
  4. "fmt"
  5. "go/ast"
  6. "go/parser"
  7. "go/token"
  8. "os"
  9. "path/filepath"
  10. "runtime"
  11. "strings"
  12. "github.com/uozi-tech/cosy/logger"
  13. )
  14. // Structure for notification function calls
  15. type NotificationCall struct {
  16. Type string
  17. Title string
  18. Content string
  19. Path string
  20. }
  21. // Directories to exclude
  22. var excludeDirs = []string{
  23. ".devcontainer", ".github", ".idea", ".pnpm-store",
  24. ".vscode", "app", "query", "tmp", "cmd", ".git", ".go", ".claude",
  25. ".cunzhi-memory", ".cursor", ".github", ".idea",
  26. ".vscode", ".pnpm-store",
  27. }
  28. // Main function
  29. func main() {
  30. logger.Init("release")
  31. // Start scanning from the project root
  32. _, file, _, ok := runtime.Caller(0)
  33. if !ok {
  34. logger.Error("Unable to get the current file")
  35. return
  36. }
  37. root := filepath.Join(filepath.Dir(file), "../../")
  38. calls := []NotificationCall{}
  39. // Scan all Go files
  40. err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
  41. if err != nil {
  42. return err
  43. }
  44. // Skip excluded directories
  45. for _, dir := range excludeDirs {
  46. if strings.Contains(path, dir) {
  47. if info.IsDir() {
  48. return filepath.SkipDir
  49. }
  50. return nil
  51. }
  52. }
  53. // Only process Go files
  54. if !info.IsDir() && strings.HasSuffix(path, ".go") {
  55. findNotificationCalls(path, &calls)
  56. }
  57. return nil
  58. })
  59. if err != nil {
  60. logger.Errorf("Error walking the path: %v\n", err)
  61. return
  62. }
  63. // Generate a single TS file
  64. generateSingleTSFile(root, calls)
  65. logger.Infof("Found %d notification calls\n", len(calls))
  66. }
  67. // Find notification function calls in Go files
  68. func findNotificationCalls(filePath string, calls *[]NotificationCall) {
  69. // Parse Go code
  70. fset := token.NewFileSet()
  71. node, err := parser.ParseFile(fset, filePath, nil, parser.ParseComments)
  72. if err != nil {
  73. logger.Errorf("Error parsing %s: %v\n", filePath, err)
  74. return
  75. }
  76. // Check if this file is in the notification package
  77. isNotificationPackage := strings.Contains(filePath, "internal/notification") ||
  78. strings.Contains(filePath, "notification/")
  79. // Traverse the AST to find function calls
  80. ast.Inspect(node, func(n ast.Node) bool {
  81. callExpr, ok := n.(*ast.CallExpr)
  82. if !ok {
  83. return true
  84. }
  85. var funcName string
  86. var isTargetCall bool
  87. // Check if it's a call to the notification package (notification.Info)
  88. if selExpr, ok := callExpr.Fun.(*ast.SelectorExpr); ok {
  89. if xident, ok := selExpr.X.(*ast.Ident); ok && xident.Name == "notification" {
  90. funcName = selExpr.Sel.Name
  91. isTargetCall = funcName == "Info" || funcName == "Error" || funcName == "Warning" || funcName == "Success" || funcName == "Define"
  92. }
  93. } else if isNotificationPackage {
  94. // Check if it's a direct function call within the notification package (Info, Error, etc.)
  95. if ident, ok := callExpr.Fun.(*ast.Ident); ok {
  96. funcName = ident.Name
  97. isTargetCall = funcName == "Info" || funcName == "Error" || funcName == "Warning" || funcName == "Success" || funcName == "Define"
  98. }
  99. }
  100. if isTargetCall {
  101. // Function must have at least two parameters (title, content)
  102. if len(callExpr.Args) >= 2 {
  103. titleArg := callExpr.Args[0]
  104. contentArg := callExpr.Args[1]
  105. // Get parameter values
  106. title := getStringValue(titleArg)
  107. content := getStringValue(contentArg)
  108. // Ignore cases where content is a variable name or function call
  109. if content != "" && !isVariableOrFunctionCall(content) {
  110. *calls = append(*calls, NotificationCall{
  111. Type: funcName,
  112. Title: title,
  113. Content: content,
  114. Path: filePath,
  115. })
  116. }
  117. }
  118. }
  119. return true
  120. })
  121. }
  122. // Check if the string is a variable name or function call
  123. func isVariableOrFunctionCall(s string) bool {
  124. // Simple check: if the string doesn't contain spaces or quotes, it might be a variable name
  125. if !strings.Contains(s, " ") && !strings.Contains(s, "\"") && !strings.Contains(s, "'") {
  126. return true
  127. }
  128. // If it looks like a function call, e.g., err.Error()
  129. if strings.Contains(s, "(") && strings.Contains(s, ")") {
  130. return true
  131. }
  132. return false
  133. }
  134. // Get string value from AST node
  135. func getStringValue(expr ast.Expr) string {
  136. // Direct string
  137. if lit, ok := expr.(*ast.BasicLit); ok && lit.Kind == token.STRING {
  138. // Return string without quotes
  139. return strings.Trim(lit.Value, "\"")
  140. }
  141. // Recover string value from source code expression
  142. var str strings.Builder
  143. if bin, ok := expr.(*ast.BinaryExpr); ok {
  144. // Handle string concatenation expression
  145. leftStr := getStringValue(bin.X)
  146. rightStr := getStringValue(bin.Y)
  147. str.WriteString(leftStr)
  148. str.WriteString(rightStr)
  149. }
  150. if str.Len() > 0 {
  151. return str.String()
  152. }
  153. // Return empty string if unable to parse as string
  154. return ""
  155. }
  156. // Generate a single TypeScript file
  157. func generateSingleTSFile(root string, calls []NotificationCall) {
  158. // Create target directory
  159. targetDir := filepath.Join(root, "app/src/components/Notification")
  160. err := os.MkdirAll(targetDir, 0755)
  161. if err != nil {
  162. logger.Errorf("Error creating directory %s: %v\n", targetDir, err)
  163. return
  164. }
  165. // Create file name
  166. tsFilePath := filepath.Join(targetDir, "notifications.ts")
  167. // Prepare file content
  168. var content strings.Builder
  169. content.WriteString("// Auto-generated notification texts\n")
  170. content.WriteString("// Extracted from Go source code notification function calls\n")
  171. content.WriteString("/* eslint-disable ts/no-explicit-any */\n\n")
  172. content.WriteString("const notifications: Record<string, { title: () => string, content: (args: any) => string }> = {\n")
  173. // Track used keys to avoid duplicates
  174. usedKeys := make(map[string]bool)
  175. // Organize notifications by directory
  176. messagesByDir := make(map[string][]NotificationCall)
  177. for _, call := range calls {
  178. dir := filepath.Dir(call.Path)
  179. // Extract module name from directory path
  180. dirParts := strings.Split(dir, "/")
  181. moduleName := dirParts[len(dirParts)-1]
  182. if strings.HasPrefix(dir, "internal/") || strings.HasPrefix(dir, "api/") {
  183. messagesByDir[moduleName] = append(messagesByDir[moduleName], call)
  184. } else {
  185. messagesByDir["general"] = append(messagesByDir["general"], call)
  186. }
  187. }
  188. // Add comments for each module and write notifications
  189. for module, moduleCalls := range messagesByDir {
  190. content.WriteString(fmt.Sprintf("\n // %s module notifications\n", module))
  191. for _, call := range moduleCalls {
  192. // Escape quotes in title and content
  193. escapedTitle := strings.ReplaceAll(call.Title, "'", "\\'")
  194. escapedContent := strings.ReplaceAll(call.Content, "'", "\\'")
  195. // Use just the title as the key
  196. key := call.Title
  197. // Check if key is already used, generate unique key if necessary
  198. uniqueKey := key
  199. counter := 1
  200. for usedKeys[uniqueKey] {
  201. uniqueKey = fmt.Sprintf("%s_%d", key, counter)
  202. counter++
  203. }
  204. usedKeys[uniqueKey] = true
  205. // Write record with both title and content as functions
  206. content.WriteString(fmt.Sprintf(" '%s': {\n", uniqueKey))
  207. content.WriteString(fmt.Sprintf(" title: () => $gettext('%s'),\n", escapedTitle))
  208. content.WriteString(fmt.Sprintf(" content: (args: any) => $gettext('%s', args, true),\n", escapedContent))
  209. content.WriteString(" },\n")
  210. }
  211. }
  212. content.WriteString("}\n\n")
  213. content.WriteString("export default notifications\n")
  214. // Write file
  215. err = os.WriteFile(tsFilePath, []byte(content.String()), 0644)
  216. if err != nil {
  217. logger.Errorf("Error writing TS file %s: %v\n", tsFilePath, err)
  218. return
  219. }
  220. logger.Infof("Generated single TS file: %s with %d notifications\n", tsFilePath, len(calls))
  221. }