go-store/conventions_test.go
Virgil 039260fcf6 test(ax): enforce exported field usage examples
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-04 09:48:02 +00:00

288 lines
7.2 KiB
Go

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<File>_<Function>_<Good|Bad|Ugly>")
}
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()
}