1
0

generate.go 7.3 KB

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