151 lines
4.0 KiB
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
|
|
}
|