1
0

generate.go 7.5 KB

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