Check resolved types at the public boundary

This commit is contained in:
2026-09-12 17:23:47 +00:00
parent 7395f54513
commit 6102b36adf
3 changed files with 241 additions and 93 deletions

View File

@@ -1,7 +1,6 @@
package configkit
import (
"go/ast"
"go/parser"
"go/token"
"io/fs"
@@ -9,7 +8,6 @@ import (
"strconv"
"strings"
"testing"
"unicode"
)
func TestDependencyImportBoundary(t *testing.T) {
@@ -59,92 +57,3 @@ func TestDependencyImportBoundary(t *testing.T) {
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
}