Files
ConfigKit/import_boundary_test.go

151 lines
4.0 KiB
Go

package configkit
import (
"go/ast"
"go/parser"
"go/token"
"io/fs"
"path/filepath"
"strconv"
"strings"
"testing"
"unicode"
)
func TestDependencyImportBoundary(t *testing.T) {
t.Parallel()
err := filepath.WalkDir(".", func(path string, entry fs.DirEntry, walkErr error) error {
if walkErr != nil {
return walkErr
}
if entry.IsDir() {
if path == ".git" {
return filepath.SkipDir
}
return nil
}
if filepath.Ext(path) != ".go" {
return nil
}
file, err := parser.ParseFile(token.NewFileSet(), path, nil, parser.ImportsOnly)
if err != nil {
return err
}
for _, imported := range file.Imports {
importPath, err := strconv.Unquote(imported.Path.Value)
if err != nil {
return err
}
if strings.HasPrefix(importPath, "go.yaml.in/yaml/") && !strings.HasPrefix(filepath.ToSlash(path), "internal/yaml/") {
t.Errorf("%s imports private YAML dependency %s", path, importPath)
}
if strings.HasPrefix(importPath, "github.com/google/go-cmp/") && !strings.HasSuffix(path, "_test.go") {
t.Errorf("%s imports test-only dependency %s", path, importPath)
}
production := !strings.HasSuffix(path, "_test.go")
rootProduction := production && filepath.Dir(path) == "."
internalProduction := production && strings.HasPrefix(filepath.ToSlash(path), "internal/")
if rootProduction && strings.HasPrefix(importPath, "gitea.maximumdirect.net/eric/ConfigKit/internal/") && importPath != "gitea.maximumdirect.net/eric/ConfigKit/internal/engine" {
t.Errorf("%s bypasses the engine boundary through %s", path, importPath)
}
if internalProduction && importPath == "gitea.maximumdirect.net/eric/ConfigKit" {
t.Errorf("%s imports the public root package", path)
}
}
return nil
})
if err != nil {
t.Fatal(err)
}
}
func TestPublicFacadeBoundary(t *testing.T) {
t.Parallel()
entries, err := filepath.Glob("*.go")
if err != nil {
t.Fatal(err)
}
for _, path := range entries {
if strings.HasSuffix(path, "_test.go") {
continue
}
file, err := parser.ParseFile(token.NewFileSet(), path, nil, 0)
if err != nil {
t.Fatal(err)
}
imports := importedPaths(file)
for _, declaration := range file.Decls {
switch declaration := declaration.(type) {
case *ast.FuncDecl:
if exportedIdentifier(declaration.Name.Name) && usesInternalType(declaration.Type, imports) {
t.Errorf("%s exposes an internal type in its signature", declaration.Name)
}
case *ast.GenDecl:
for _, specification := range declaration.Specs {
typeSpec, ok := specification.(*ast.TypeSpec)
if !ok || !exportedIdentifier(typeSpec.Name.Name) {
continue
}
if typeSpec.Assign.IsValid() {
t.Errorf("%s is an exported type alias", typeSpec.Name)
}
structure, ok := typeSpec.Type.(*ast.StructType)
if !ok {
continue
}
for _, field := range structure.Fields.List {
exposed := len(field.Names) == 0
for _, name := range field.Names {
exposed = exposed || exportedIdentifier(name.Name)
}
if exposed && usesInternalType(field.Type, imports) {
t.Errorf("%s exposes an internal type through a field", typeSpec.Name)
}
}
}
}
}
}
}
func importedPaths(file *ast.File) map[string]string {
paths := make(map[string]string, len(file.Imports))
for _, imported := range file.Imports {
path, err := strconv.Unquote(imported.Path.Value)
if err != nil {
continue
}
name := filepath.Base(path)
if imported.Name != nil {
name = imported.Name.Name
}
paths[name] = path
}
return paths
}
func usesInternalType(expression ast.Expr, imports map[string]string) bool {
internal := false
ast.Inspect(expression, func(node ast.Node) bool {
selector, ok := node.(*ast.SelectorExpr)
if !ok {
return true
}
packageName, ok := selector.X.(*ast.Ident)
if ok && strings.Contains(imports[packageName.Name], "/internal/") {
internal = true
}
return !internal
})
return internal
}
func exportedIdentifier(name string) bool {
for _, character := range name {
return unicode.IsUpper(character)
}
return false
}