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>
144 lines
4.0 KiB
Go
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")
|
|
}
|