check-safety/sqlc_uuid.go
Alex Dunmow cd88c808b0 initial: standalone check-safety module hoisted from CMS
Static safety/lint runner for the BlockNinja codebase. ~25 invariant
checks across Go and frontend sources. Was at git.dev.alexdunmow.com:block/ninja
in backend/cmd/check-safety/ until the 2026-06-06 consolidation moved
the BlockNinja repos under a shared ~/src/blockninja/ parent.

This repo is the standalone extraction:
- Own go.mod (git.dev.alexdunmow.com/block/check-safety, go 1.26.4)
- Vendored internal/{helpers,theme} from CMS (Go's internal/ rule
  blocks cross-module imports; vendoring is the workaround)
- CLI contract unchanged: `check-safety <target-dir> [--flags]`
- CMS Makefile shells into ../check-safety for safety-check /
  install-safety-checker targets

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-06 13:04:02 +08:00

178 lines
3.8 KiB
Go

package main
import (
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"gopkg.in/yaml.v3"
)
type sqlcUUIDViolation struct {
file string
rule string
detail string
}
type sqlcConfigFile struct {
SQL []sqlcConfigEntry `yaml:"sql"`
}
type sqlcConfigEntry struct {
Gen sqlcGenConfig `yaml:"gen"`
}
type sqlcGenConfig struct {
Go sqlcGoConfig `yaml:"go"`
}
type sqlcGoConfig struct {
Overrides []sqlcOverride `yaml:"overrides"`
}
type sqlcOverride struct {
DBType string `yaml:"db_type"`
Nullable bool `yaml:"nullable"`
GoType sqlcGoType `yaml:"go_type"`
}
type sqlcGoType struct {
Scalar string
Import string
Type string
}
func (g *sqlcGoType) UnmarshalYAML(node *yaml.Node) error {
switch node.Kind {
case yaml.ScalarNode:
return node.Decode(&g.Scalar)
case yaml.MappingNode:
var decoded struct {
Import string `yaml:"import"`
Type string `yaml:"type"`
}
if err := node.Decode(&decoded); err != nil {
return err
}
g.Import = decoded.Import
g.Type = decoded.Type
return nil
default:
return fmt.Errorf("unsupported go_type yaml node kind %d", node.Kind)
}
}
func (g sqlcGoType) matchesGoogleUUID(nullable bool) bool {
if nullable {
return g.Scalar == "github.com/google/uuid.NullUUID" ||
(g.Import == "github.com/google/uuid" && g.Type == "NullUUID")
}
return g.Scalar == "github.com/google/uuid.UUID" ||
(g.Import == "github.com/google/uuid" && g.Type == "UUID")
}
func findSQLCConfigFiles(root string, includeRoot bool) []string {
var files []string
_ = filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if err != nil {
return nil
}
if info.IsDir() {
switch info.Name() {
case ".git", "vendor", "node_modules", "dist":
return filepath.SkipDir
}
return nil
}
base := filepath.Base(path)
if base != "sqlc.yaml" && base != "sqlc.yml" {
return nil
}
if includeRoot && samePath(path, filepath.Join(root, base)) {
files = append(files, path)
return nil
}
rel, relErr := filepath.Rel(root, path)
if relErr != nil {
return nil
}
if strings.Contains(filepath.ToSlash(rel), "/cmd/") || strings.HasPrefix(filepath.ToSlash(rel), "cmd/") {
files = append(files, path)
}
return nil
})
sort.Strings(files)
return files
}
func checkSQLCUUIDOverrides(root string, includeRoot bool) []sqlcUUIDViolation {
var violations []sqlcUUIDViolation
for _, path := range findSQLCConfigFiles(root, includeRoot) {
data, err := os.ReadFile(path)
if err != nil {
violations = append(violations, sqlcUUIDViolation{
file: displayLintPath(root, path),
rule: "sqlc-read",
detail: fmt.Sprintf("failed to read sqlc config: %v", err),
})
continue
}
var cfg sqlcConfigFile
if err := yaml.Unmarshal(data, &cfg); err != nil {
violations = append(violations, sqlcUUIDViolation{
file: displayLintPath(root, path),
rule: "sqlc-parse",
detail: fmt.Sprintf("failed to parse sqlc config: %v", err),
})
continue
}
hasUUID := false
hasNullUUID := false
for _, block := range cfg.SQL {
for _, override := range block.Gen.Go.Overrides {
if override.DBType != "uuid" {
continue
}
if override.Nullable {
if override.GoType.matchesGoogleUUID(true) {
hasNullUUID = true
}
continue
}
if override.GoType.matchesGoogleUUID(false) {
hasUUID = true
}
}
}
relPath := displayLintPath(root, path)
if !hasUUID {
violations = append(violations, sqlcUUIDViolation{
file: relPath,
rule: "sqlc-uuid-type",
detail: `db_type "uuid" must map to github.com/google/uuid.UUID`,
})
}
if !hasNullUUID {
violations = append(violations, sqlcUUIDViolation{
file: relPath,
rule: "sqlc-null-uuid-type",
detail: `nullable db_type "uuid" must map to github.com/google/uuid.NullUUID`,
})
}
}
return violations
}