Files
ConfigKit/public_boundary_test.go

232 lines
7.1 KiB
Go

package configkit
import (
"bytes"
"encoding/json"
"go/ast"
"go/importer"
"go/parser"
"go/token"
"go/types"
"io"
"os"
"os/exec"
"strings"
"testing"
)
func TestPublicFacadeBoundary(t *testing.T) {
t.Parallel()
// Use the active toolchain's build selection and export data, not a second
// source-level approximation of aliases, generics, or promoted method sets.
command := exec.Command("go", "list", "-export", "-deps", "-json", ".")
command.Env = append(os.Environ(), "GOPROXY=off")
var stderr bytes.Buffer
command.Stderr = &stderr
output, err := command.Output()
if err != nil {
t.Fatalf("load package exports: %v\n%s", err, &stderr)
}
exports := make(map[string]string)
var root string
decoder := json.NewDecoder(bytes.NewReader(output))
for {
var entry struct {
ImportPath string
Export string
DepOnly bool
}
if err := decoder.Decode(&entry); err == io.EOF {
break
} else if err != nil {
t.Fatal(err)
}
exports[entry.ImportPath] = entry.Export
if !entry.DepOnly {
root = entry.ImportPath
}
}
loader := importer.ForCompiler(token.NewFileSet(), "gc", func(path string) (io.ReadCloser, error) {
return os.Open(exports[path])
})
pkg, err := loader.Import(root)
if err != nil {
t.Fatal(err)
}
for _, name := range publicBoundaryViolations(pkg) {
t.Errorf("%s exposes an internal type or is a public type alias", name)
}
}
func publicBoundaryViolations(pkg *types.Package) []string {
var violations []string
for _, name := range pkg.Scope().Names() {
object := pkg.Scope().Lookup(name)
if !object.Exported() {
continue
}
if named, ok := object.(*types.TypeName); ok && named.IsAlias() {
violations = append(violations, name)
continue
}
if exposesInternalType(object.Type(), pkg, make(map[types.Type]bool)) {
violations = append(violations, name)
}
}
return violations
}
func exposesInternalType(value types.Type, root *types.Package, seen map[types.Type]bool) bool {
if value == nil || seen[value] {
return false
}
seen[value] = true
visit := func(child types.Type) bool { return exposesInternalType(child, root, seen) }
parameters := func(list *types.TypeParamList) bool {
for i := 0; i < list.Len(); i++ {
if visit(list.At(i)) {
return true
}
}
return false
}
switch value := value.(type) {
case *types.Alias:
return visit(types.Unalias(value))
case *types.Named:
owner := value.Obj().Pkg()
if owner != nil && (strings.HasPrefix(owner.Path(), "internal/") || strings.Contains(owner.Path(), "/internal/")) {
return true
}
for i := 0; i < value.TypeArgs().Len(); i++ {
if visit(value.TypeArgs().At(i)) {
return true
}
}
// Other public packages own their implementation boundaries. Only root
// named types need their exported fields, methods, and constraints walked.
if owner != root {
return false
}
if parameters(value.TypeParams()) || visit(value.Underlying()) {
return true
}
for _, receiver := range []types.Type{value, types.NewPointer(value)} {
methods := types.NewMethodSet(receiver)
for i := 0; i < methods.Len(); i++ {
method := methods.At(i).Obj()
if method.Exported() && visit(method.Type()) {
return true
}
}
}
case *types.Pointer:
return visit(value.Elem())
case *types.Slice:
return visit(value.Elem())
case *types.Array:
return visit(value.Elem())
case *types.Map:
return visit(value.Key()) || visit(value.Elem())
case *types.Chan:
return visit(value.Elem())
case *types.Struct:
for i := 0; i < value.NumFields(); i++ {
field := value.Field(i)
if (field.Exported() || field.Embedded()) && visit(field.Type()) {
return true
}
}
case *types.Signature:
return parameters(value.TypeParams()) || parameters(value.RecvTypeParams()) || visit(value.Params()) || visit(value.Results())
case *types.Tuple:
for i := 0; i < value.Len(); i++ {
if visit(value.At(i).Type()) {
return true
}
}
case *types.Interface:
for i := 0; i < value.NumMethods(); i++ {
method := value.Method(i)
if method.Exported() && visit(method.Type()) {
return true
}
}
for i := 0; i < value.NumEmbeddeds(); i++ {
if visit(value.EmbeddedType(i)) {
return true
}
}
case *types.TypeParam:
return visit(value.Constraint())
case *types.Union:
for i := 0; i < value.Len(); i++ {
if visit(value.Term(i).Type()) {
return true
}
}
}
return false
}
type boundaryFixtureImporter struct{ engine *types.Package }
func (i boundaryFixtureImporter) Import(path string) (*types.Package, error) {
return i.engine, nil
}
func TestPublicBoundaryDeclarations(t *testing.T) {
t.Parallel()
fset := token.NewFileSet()
engineFile, err := parser.ParseFile(fset, "engine.go", "package engine; type Path struct{}", 0)
if err != nil {
t.Fatal(err)
}
engine, err := new(types.Config).Check("fixture/internal/engine", fset, []*ast.File{engineFile}, nil)
if err != nil {
t.Fatal(err)
}
for _, test := range []struct {
name, declaration string
exposed bool
}{
{"collection", "type API []engine.Path", true},
{"alias result", "type hidden = engine.Path; func API() hidden { panic(0) }", true},
{"constraint", "type API[T interface{ ~[]engine.Path }] struct{}", true},
{"alias", "type API = engine.Path", true},
{"field", "type API struct { Value engine.Path }", true},
{"embedding", "type API struct { engine.Path }", true},
{"function type", "type API func(engine.Path)", true},
{"interface", "type API interface { Value() engine.Path }", true},
{"pointer method", "type API struct{}; func (*API) Value() engine.Path { panic(0) }", true},
{"promoted method", "type hidden struct{}; func (*hidden) Value() engine.Path { panic(0) }; type API struct{ hidden }", true},
{"nested record", "type hidden struct { Values map[string][2]engine.Path }; type API struct { Value *hidden }", true},
{"channel", "var API chan engine.Path", true},
{"function constraint", "func API[T interface{ ~[]engine.Path }]() {}", true},
{"generic argument", "type hidden[T any] struct{ value T }; func API() hidden[engine.Path] { panic(0) }", true},
{"wrapper", "type API struct{ value engine.Path }; func (API) String() string { return \"\" }", false},
{"public record", "type Path struct{ value engine.Path }; type API struct{ Paths []Path }", false},
{"generic wrapper", "type API[T any] struct{ Config T; value engine.Path }", false},
{"recursive wrapper", "type API struct{ Next *API; value engine.Path }", false},
{"private helper", "type hidden = engine.Path; type API struct{ value hidden }", false},
} {
t.Run(test.name, func(t *testing.T) {
// Separate declarations also ensure resolution is package-wide.
file, err := parser.ParseFile(fset, "fixture.go", "package facade; import \"fixture/internal/engine\"; var _ engine.Path; "+test.declaration, 0)
if err != nil {
t.Fatal(err)
}
config := types.Config{Importer: boundaryFixtureImporter{engine}}
pkg, err := config.Check("fixture", fset, []*ast.File{file}, nil)
if err != nil {
t.Fatal(err)
}
got := publicBoundaryViolations(pkg)
if (len(got) != 0) != test.exposed {
t.Fatalf("violations = %v; want exposed=%v", got, test.exposed)
}
})
}
}