check-safety/rpcerrors.go
Alex Dunmow c4d01de82c feat: concise output + diff-scoped any check
Reporter is now a collector: checks declare a verdict (OK/Skip/Warn/Fail/
Fatal) plus findings, and a single central render() prints output. Default
output is silent on pass/skip — only FAIL/WARN/ERR checks print, followed by
one tally line, so a clean run is two lines. --verbose restores full per-check
output. All ~30 checks were converted to this API; orphaned guidance/label
helpers (printPerTargetOKLines, per-check *Help blocks, colors_format.go) were
removed.

The any-usage check (2e) now defaults to only the unstaged working-tree diff
(changed lines), via a new per-repo git-diff index in changedlines.go; --all-any
restores the full scan. Not-a-git-repo / no-diff warns on nothing.

Golden fixtures regenerated; integration tests updated to the new format; added
unit tests for the diff index.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 01:16:56 +08:00

144 lines
4.0 KiB
Go

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")
}