package main import ( "os" "path/filepath" "regexp" "strings" ) type rpcErrorViolation struct { file string line int rule string snippet string } var ( // Matches: const { ... } = useQuery — destructuring pattern reQueryDestructure = regexp.MustCompile(`(?:const|let|var)\s+\{([^}]+)\}\s*=\s*useQuery`) // Matches: useMutation( — start of a mutation hook call reUseMutation = regexp.MustCompile(`\buseMutation\s*\(`) // Matches: onError in options reOnError = regexp.MustCompile(`onError\s*:`) // Matches: catch or .catch( reCatch = regexp.MustCompile(`\bcatch\b`) // Matches: toast.error reToastError = regexp.MustCompile(`toast\.error\s*\(`) ) // checkRPCErrors detects missing error handling for RPC queries and mutations. func checkRPCErrors(webSrcDir string) (violations []rpcErrorViolation, warnings []rpcErrorViolation) { globalMutationHandler := hasGlobalMutationErrorHandler(webSrcDir) if err := filepath.Walk(webSrcDir, func(path string, info os.FileInfo, err error) error { if err != nil || info.IsDir() { return nil } ext := filepath.Ext(path) if ext != ".tsx" && ext != ".ts" { return nil } if strings.Contains(path, "node_modules") || strings.Contains(path, "/dist/") { return nil } relPath, _ := filepath.Rel(webSrcDir, path) // Only check components/ and routes/ if !strings.HasPrefix(relPath, "components/") && !strings.HasPrefix(relPath, "routes/") { return nil } data, err := os.ReadFile(path) if err != nil { return nil } lines := strings.Split(string(data), "\n") fullContent := string(data) for i, line := range lines { lineNum := i + 1 trimmed := strings.TrimSpace(line) // Skip comments if strings.HasPrefix(trimmed, "//") || strings.HasPrefix(trimmed, "*") || strings.HasPrefix(trimmed, "{/*") { continue } // Check useQuery destructuring for missing error if reQueryDestructure.MatchString(line) { matches := reQueryDestructure.FindStringSubmatch(line) if len(matches) > 1 { destructured := matches[1] if !strings.Contains(destructured, "error") { warnings = append(warnings, rpcErrorViolation{ file: relPath, line: lineNum, rule: "query-no-error", snippet: strings.TrimSpace(line), }) } } } // Check useMutation — find mutations with no error handling anywhere in file if reUseMutation.MatchString(line) && !strings.HasPrefix(trimmed, "//") { // Check if this mutation's options or its call sites have error handling // Look in the surrounding context (next 20 lines) for onError hasErrorHandling := false // Check the mutation definition options (same line or next few lines) for j := i; j < len(lines) && j <= i+15; j++ { if reOnError.MatchString(lines[j]) { hasErrorHandling = true break } // Stop at next statement if j > i && (strings.Contains(lines[j], "const ") || strings.Contains(lines[j], "return ")) { break } } // Check if the file has ANY try/catch or toast.error (broad heuristic) if !hasErrorHandling { if reCatch.MatchString(fullContent) || reToastError.MatchString(fullContent) { // File has some error handling — mutation call sites likely handle it hasErrorHandling = true } } if !hasErrorHandling && !globalMutationHandler { violations = append(violations, rpcErrorViolation{ file: relPath, line: lineNum, rule: "mutation-no-error-handler", snippet: strings.TrimSpace(line), }) } } } return nil }); err != nil { _ = err // Walk errors are non-fatal for safety checks } return violations, warnings } func hasGlobalMutationErrorHandler(webSrcDir string) bool { queryClientPath := filepath.Join(webSrcDir, "lib", "query-client.ts") data, err := os.ReadFile(queryClientPath) if err != nil { return false } content := string(data) return strings.Contains(content, "MutationCache") && strings.Contains(content, "onError") }