Skip to content

Instantly share code, notes, and snippets.

@NSMyself
Last active July 24, 2025 16:47
Show Gist options
  • Save NSMyself/e632850b223a6f84ccbdf3b80b08ee10 to your computer and use it in GitHub Desktop.
Save NSMyself/e632850b223a6f84ccbdf3b80b08ee10 to your computer and use it in GitHub Desktop.
// Package storecheck defines an analyzer that enforces Store struct visibility
// in business/core/<packageName>/stores/db/db.go files
package storecheck
import (
"fmt"
"go/ast"
"go/token"
"path/filepath"
"regexp"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector"
)
var Analyzer = &analysis.Analyzer{
Name: "xm_store_check",
Doc: "enforces that Store structs are public in db.go files",
Run: run,
Requires: []*analysis.Analyzer{inspect.Analyzer},
}
// pathPattern matches business/core/<packageName>/stores/db/db.go
// where packageName is any valid Go identifier
var pathPattern = regexp.MustCompile(`business/core/[a-zA-Z_][a-zA-Z0-9_]*/stores/db/db\.go$`)
func run(pass *analysis.Pass) (interface{}, error) {
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
// Filter to only check files that match our target pattern
targetFiles := make(map[*ast.File]bool)
for _, file := range pass.Files {
pos := pass.Fset.Position(file.Pos())
filename := pos.Filename
// Normalize path separators for cross-platform compatibility
normalizedPath := filepath.ToSlash(filename)
// DEBUG: Print all files being processed
fmt.Printf("DEBUG: Processing file: %s\n", normalizedPath)
if pathPattern.MatchString(normalizedPath) {
fmt.Printf("DEBUG: File matches pattern: %s\n", normalizedPath)
targetFiles[file] = true
}
}
// If no target files found, nothing to check
if len(targetFiles) == 0 {
return nil, nil
}
// Look for type declarations
nodeFilter := []ast.Node{
(*ast.GenDecl)(nil),
}
inspect.Preorder(nodeFilter, func(n ast.Node) {
genDecl := n.(*ast.GenDecl)
// Only interested in type declarations
if genDecl.Tok != token.TYPE {
return
}
// Check if this declaration is in one of our target files
filePos := pass.Fset.Position(genDecl.Pos())
var isTargetFile bool
for file := range targetFiles {
if pass.Fset.Position(file.Pos()).Filename == filePos.Filename {
isTargetFile = true
break
}
}
if !isTargetFile {
return
}
// Check each type spec in the declaration
for _, spec := range genDecl.Specs {
typeSpec, ok := spec.(*ast.TypeSpec)
if !ok {
continue
}
// DEBUG: Print all struct names found
if _, ok := typeSpec.Type.(*ast.StructType); ok {
fmt.Printf("DEBUG: Found struct: %s (exported: %v)\n", typeSpec.Name.Name, typeSpec.Name.IsExported())
}
// Check if this is a struct that should be named "Store"
// We want to catch both "store" (incorrect) and "Store" (correct)
structName := typeSpec.Name.Name
if structName != "Store" && structName != "store" {
continue
}
// Verify it's actually a struct type
if _, ok := typeSpec.Type.(*ast.StructType); !ok {
continue
}
fmt.Printf("DEBUG: Found Store struct, exported: %v\n", typeSpec.Name.IsExported())
// Check if Store struct is not exported (starts with lowercase)
if !typeSpec.Name.IsExported() {
pass.Reportf(typeSpec.Pos(), "Store struct must be public in db.go files")
}
}
})
return nil, nil
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment