package store import ( "go/ast" "go/parser" "go/token" "io/fs" "slices" "testing" "unicode" core "dappco.re/go/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestConventions_Imports_Good_Banned(t *testing.T) { files := repoGoFiles(t, func(name string) bool { return core.HasSuffix(name, ".go") }) bannedImports := []string{ "encoding/json", "errors", "fmt", "os", "os/exec", "path/filepath", "strings", } var banned []string for _, path := range files { file := parseGoFile(t, path) for _, spec := range file.Imports { importPath := trimImportPath(spec.Path.Value) if core.HasPrefix(importPath, "forge.lthn.ai/") || slices.Contains(bannedImports, importPath) { banned = append(banned, core.Concat(path, ": ", importPath)) } } } slices.Sort(banned) assert.Empty(t, banned, "banned imports should not appear in repository Go files") } func TestConventions_TestNaming_Good_StrictPattern(t *testing.T) { files := repoGoFiles(t, func(name string) bool { return core.HasSuffix(name, "_test.go") }) allowedClasses := []string{"Good", "Bad", "Ugly"} var invalid []string for _, path := range files { expectedPrefix := testNamePrefix(path) file := parseGoFile(t, path) for _, decl := range file.Decls { fn, ok := decl.(*ast.FuncDecl) if !ok || fn.Recv != nil { continue } name := fn.Name.Name if !core.HasPrefix(name, "Test") || name == "TestMain" { continue } if !core.HasPrefix(name, expectedPrefix) { invalid = append(invalid, core.Concat(path, ": ", name)) continue } parts := core.Split(core.TrimPrefix(name, expectedPrefix), "_") if len(parts) < 2 || parts[0] == "" || !slices.Contains(allowedClasses, parts[1]) { invalid = append(invalid, core.Concat(path, ": ", name)) } } } slices.Sort(invalid) assert.Empty(t, invalid, "top-level tests must follow Test__") } func TestConventions_Exports_Good_UsageExamples(t *testing.T) { files := repoGoFiles(t, func(name string) bool { return core.HasSuffix(name, ".go") && !core.HasSuffix(name, "_test.go") }) var missing []string for _, path := range files { file := parseGoFile(t, path) for _, decl := range file.Decls { switch node := decl.(type) { case *ast.FuncDecl: if !node.Name.IsExported() { continue } if !core.Contains(commentText(node.Doc), "Usage example:") { missing = append(missing, core.Concat(path, ": ", node.Name.Name)) } case *ast.GenDecl: for _, spec := range node.Specs { switch item := spec.(type) { case *ast.TypeSpec: if !item.Name.IsExported() { continue } if !core.Contains(commentText(item.Doc, node.Doc), "Usage example:") { missing = append(missing, core.Concat(path, ": ", item.Name.Name)) } case *ast.ValueSpec: for _, name := range item.Names { if !name.IsExported() { continue } if !core.Contains(commentText(item.Doc, node.Doc), "Usage example:") { missing = append(missing, core.Concat(path, ": ", name.Name)) } } } } } } } slices.Sort(missing) assert.Empty(t, missing, "exported declarations must include a usage example in their doc comment") } func TestConventions_Exports_Good_FieldUsageExamples(t *testing.T) { files := repoGoFiles(t, func(name string) bool { return core.HasSuffix(name, ".go") && !core.HasSuffix(name, "_test.go") }) var missing []string for _, path := range files { file := parseGoFile(t, path) for _, decl := range file.Decls { node, ok := decl.(*ast.GenDecl) if !ok { continue } for _, spec := range node.Specs { typeSpec, ok := spec.(*ast.TypeSpec) if !ok || !typeSpec.Name.IsExported() { continue } structType, ok := typeSpec.Type.(*ast.StructType) if !ok { continue } for _, field := range structType.Fields.List { for _, fieldName := range field.Names { if !fieldName.IsExported() { continue } if !core.Contains(commentText(field.Doc), "Usage example:") { missing = append(missing, core.Concat(path, ": ", typeSpec.Name.Name, ".", fieldName.Name)) } } } } } } slices.Sort(missing) assert.Empty(t, missing, "exported struct fields must include a usage example in their doc comment") } func TestConventions_Exports_Good_NoCompatibilityAliases(t *testing.T) { files := repoGoFiles(t, func(name string) bool { return core.HasSuffix(name, ".go") && !core.HasSuffix(name, "_test.go") }) var invalid []string for _, path := range files { file := parseGoFile(t, path) for _, decl := range file.Decls { switch node := decl.(type) { case *ast.GenDecl: for _, spec := range node.Specs { switch item := spec.(type) { case *ast.TypeSpec: if item.Name.Name == "KV" { invalid = append(invalid, core.Concat(path, ": ", item.Name.Name)) } if item.Name.Name != "Watcher" { continue } structType, ok := item.Type.(*ast.StructType) if !ok { continue } for _, field := range structType.Fields.List { for _, name := range field.Names { if name.Name == "Ch" { invalid = append(invalid, core.Concat(path, ": Watcher.Ch")) } } } case *ast.ValueSpec: for _, name := range item.Names { if name.Name == "ErrNotFound" || name.Name == "ErrQuotaExceeded" { invalid = append(invalid, core.Concat(path, ": ", name.Name)) } } } } } } } slices.Sort(invalid) assert.Empty(t, invalid, "legacy compatibility aliases should not appear in the public Go API") } func repoGoFiles(t *testing.T, keep func(name string) bool) []string { t.Helper() result := testFilesystem().List(".") requireCoreOK(t, result) entries, ok := result.Value.([]fs.DirEntry) require.True(t, ok, "unexpected directory entry type: %T", result.Value) var files []string for _, entry := range entries { if entry.IsDir() || !keep(entry.Name()) { continue } files = append(files, entry.Name()) } slices.Sort(files) return files } func parseGoFile(t *testing.T, path string) *ast.File { t.Helper() file, err := parser.ParseFile(token.NewFileSet(), path, nil, parser.ParseComments) require.NoError(t, err) return file } func trimImportPath(value string) string { return core.TrimSuffix(core.TrimPrefix(value, `"`), `"`) } func testNamePrefix(path string) string { return core.Concat("Test", camelCase(core.TrimSuffix(path, "_test.go")), "_") } func camelCase(value string) string { parts := core.Split(value, "_") builder := core.NewBuilder() for _, part := range parts { if part == "" { continue } builder.WriteString(upperFirst(part)) } return builder.String() } func upperFirst(value string) string { runes := []rune(value) if len(runes) == 0 { return "" } runes[0] = unicode.ToUpper(runes[0]) return string(runes) } func commentText(groups ...*ast.CommentGroup) string { builder := core.NewBuilder() for _, group := range groups { if group == nil { continue } text := core.Trim(group.Text()) if text == "" { continue } if builder.Len() > 0 { builder.WriteString("\n") } builder.WriteString(text) } return builder.String() }