Check resolved types at the public boundary
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user