398 lines
11 KiB
Go
398 lines
11 KiB
Go
package promptkit_test
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/importer"
|
|
"go/parser"
|
|
"go/token"
|
|
"go/types"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"runtime"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestPublicFacadeDoesNotExposeInternalTypes(t *testing.T) {
|
|
metadata := listedPackages(t, "-export", ".")
|
|
if len(metadata) != 1 {
|
|
t.Fatalf("root package metadata count = %d, want one", len(metadata))
|
|
}
|
|
|
|
pkg := importCompiledPackage(t, metadata[0])
|
|
violations := publicSurfaceViolations(pkg, metadata[0].ImportPath)
|
|
if len(violations) > 0 {
|
|
t.Fatalf("public facade exposes internal implementation:\n%s", strings.Join(violations, "\n"))
|
|
}
|
|
}
|
|
|
|
func TestInternalProductionPackagesDoNotImportFacade(t *testing.T) {
|
|
root := listedPackages(t, ".")
|
|
if len(root) != 1 {
|
|
t.Fatalf("root package metadata count = %d, want one", len(root))
|
|
}
|
|
|
|
var violations []string
|
|
for _, pkg := range listedPackages(t, "./internal/...") {
|
|
for _, imported := range pkg.Imports {
|
|
if imported == root[0].ImportPath {
|
|
violations = append(violations, pkg.ImportPath)
|
|
}
|
|
}
|
|
}
|
|
sort.Strings(violations)
|
|
if len(violations) > 0 {
|
|
t.Fatalf("internal production packages import the public facade:\n%s", strings.Join(violations, "\n"))
|
|
}
|
|
}
|
|
|
|
func TestPublicBoundaryCheckerFixtures(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
source string
|
|
wantReject bool
|
|
}{
|
|
{
|
|
name: "private internal state and sealed option are allowed",
|
|
source: `package facade
|
|
import "example.test/facade/internal/hidden"
|
|
type privateState struct { value hidden.Value }
|
|
type Public struct { state privateState }
|
|
type Option interface { apply(*privateState) }
|
|
`,
|
|
},
|
|
{
|
|
name: "returned private type method is rejected",
|
|
source: `package facade
|
|
import "example.test/facade/internal/hidden"
|
|
type result struct{}
|
|
func (result) Leak() hidden.Value { return hidden.Value{} }
|
|
func New() result { return result{} }
|
|
`,
|
|
wantReject: true,
|
|
},
|
|
{
|
|
name: "reachable private pointer method is rejected",
|
|
source: `package facade
|
|
import "example.test/facade/internal/hidden"
|
|
type result struct{}
|
|
func (*result) Leak() hidden.Value { return hidden.Value{} }
|
|
type Public struct { Result result }
|
|
`,
|
|
wantReject: true,
|
|
},
|
|
{
|
|
name: "internal generic argument is rejected",
|
|
source: `package facade
|
|
import "example.test/facade/internal/hidden"
|
|
type Box[T any] struct{}
|
|
func New() Box[hidden.Value] { return Box[hidden.Value]{} }
|
|
`,
|
|
wantReject: true,
|
|
},
|
|
{
|
|
name: "internal alias argument is rejected",
|
|
source: `package facade
|
|
import "example.test/facade/internal/hidden"
|
|
type Box[T any] = struct{}
|
|
func New() Box[hidden.Value] { return Box[hidden.Value]{} }
|
|
`,
|
|
wantReject: true,
|
|
},
|
|
{
|
|
name: "public generic argument and recursive methods are allowed",
|
|
source: `package facade
|
|
type Value struct{}
|
|
type Box[T any] struct{}
|
|
func New() Box[Value] { return Box[Value]{} }
|
|
type result struct{}
|
|
func NewResult() result { return result{} }
|
|
func (*result) Next() *result { return nil }
|
|
`,
|
|
},
|
|
{
|
|
name: "direct internal signature is rejected",
|
|
source: `package facade
|
|
import "example.test/facade/internal/hidden"
|
|
func Direct(value hidden.Value) {}
|
|
`,
|
|
wantReject: true,
|
|
},
|
|
{
|
|
name: "nested internal signature is rejected",
|
|
source: `package facade
|
|
import "example.test/facade/internal/hidden"
|
|
func Nested(values []map[string]hidden.Value) {}
|
|
`,
|
|
wantReject: true,
|
|
},
|
|
{
|
|
name: "internal public field is rejected",
|
|
source: `package facade
|
|
import "example.test/facade/internal/hidden"
|
|
type Public struct { Value hidden.Value }
|
|
`,
|
|
wantReject: true,
|
|
},
|
|
{
|
|
name: "internal alias is rejected",
|
|
source: `package facade
|
|
import "example.test/facade/internal/hidden"
|
|
type Public = hidden.Value
|
|
`,
|
|
wantReject: true,
|
|
},
|
|
{
|
|
name: "promoted internal method is rejected",
|
|
source: `package facade
|
|
import "example.test/facade/internal/hidden"
|
|
type wrapper struct { hidden.Value }
|
|
type Public struct { wrapper }
|
|
`,
|
|
wantReject: true,
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
pkg := fixturePackage(t, test.source)
|
|
violations := publicSurfaceViolations(pkg, "example.test/facade")
|
|
if got := len(violations) > 0; got != test.wantReject {
|
|
t.Fatalf("rejected = %t, want %t; violations = %v", got, test.wantReject, violations)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
type packageMetadata struct {
|
|
ImportPath string
|
|
Export string
|
|
Imports []string
|
|
}
|
|
|
|
func listedPackages(t *testing.T, args ...string) []packageMetadata {
|
|
t.Helper()
|
|
command := exec.Command("go", append([]string{"list", "-json"}, args...)...)
|
|
output, err := command.Output()
|
|
if err != nil {
|
|
t.Fatalf("go list %s: %v", strings.Join(args, " "), err)
|
|
}
|
|
|
|
decoder := json.NewDecoder(strings.NewReader(string(output)))
|
|
var packages []packageMetadata
|
|
for decoder.More() {
|
|
var metadata packageMetadata
|
|
if err := decoder.Decode(&metadata); err != nil {
|
|
t.Fatalf("decode go list %s: %v", strings.Join(args, " "), err)
|
|
}
|
|
packages = append(packages, metadata)
|
|
}
|
|
return packages
|
|
}
|
|
|
|
func importCompiledPackage(t *testing.T, root packageMetadata) *types.Package {
|
|
t.Helper()
|
|
exports := map[string]string{root.ImportPath: root.Export}
|
|
lookup := func(importPath string) (io.ReadCloser, error) {
|
|
export, ok := exports[importPath]
|
|
if !ok {
|
|
metadata := listedPackages(t, "-export", importPath)
|
|
if len(metadata) != 1 || metadata[0].Export == "" {
|
|
return nil, fmt.Errorf("export data unavailable for %s", importPath)
|
|
}
|
|
export = metadata[0].Export
|
|
exports[importPath] = export
|
|
}
|
|
return os.Open(export)
|
|
}
|
|
|
|
compiled := importer.ForCompiler(token.NewFileSet(), runtime.Compiler, lookup)
|
|
pkg, err := compiled.Import(root.ImportPath)
|
|
if err != nil {
|
|
t.Fatalf("import compiled root package: %v", err)
|
|
}
|
|
return pkg
|
|
}
|
|
|
|
func publicSurfaceViolations(pkg *types.Package, modulePath string) []string {
|
|
var violations []string
|
|
seen := map[types.Type]bool{}
|
|
record := func(where string, typ types.Type) {
|
|
violations = append(violations, fmt.Sprintf("%s: %s", where, typ.String()))
|
|
}
|
|
var inspect func(types.Type, string)
|
|
var inspectTuple func(*types.Tuple, string)
|
|
var inspectTypeParams func(*types.TypeParamList, string)
|
|
var inspectTypeArgs func(*types.TypeList, string)
|
|
var inspectMethods func(*types.Named, string)
|
|
inspect = func(typ types.Type, where string) {
|
|
if typ == nil || seen[typ] {
|
|
return
|
|
}
|
|
seen[typ] = true
|
|
|
|
switch value := typ.(type) {
|
|
case *types.Named:
|
|
inspectTypeArgs(value.TypeArgs(), where)
|
|
if isInternalPackage(value.Obj().Pkg(), modulePath) {
|
|
record(where, value)
|
|
return
|
|
}
|
|
if value.Obj().Pkg() == pkg {
|
|
inspectTypeParams(value.TypeParams(), where)
|
|
inspect(value.Underlying(), where)
|
|
inspectMethods(value, where)
|
|
}
|
|
case *types.Alias:
|
|
inspectTypeArgs(value.TypeArgs(), where)
|
|
if isInternalPackage(value.Obj().Pkg(), modulePath) {
|
|
record(where, value)
|
|
return
|
|
}
|
|
inspectTypeParams(value.TypeParams(), where)
|
|
inspect(types.Unalias(value), where)
|
|
case *types.Pointer:
|
|
inspect(value.Elem(), where)
|
|
case *types.Array:
|
|
inspect(value.Elem(), where)
|
|
case *types.Slice:
|
|
inspect(value.Elem(), where)
|
|
case *types.Map:
|
|
inspect(value.Key(), where)
|
|
inspect(value.Elem(), where)
|
|
case *types.Chan:
|
|
inspect(value.Elem(), where)
|
|
case *types.Struct:
|
|
for index := 0; index < value.NumFields(); index++ {
|
|
field := value.Field(index)
|
|
if field.Exported() {
|
|
inspect(field.Type(), where+"."+field.Name())
|
|
}
|
|
}
|
|
case *types.Interface:
|
|
value.Complete()
|
|
for index := 0; index < value.NumEmbeddeds(); index++ {
|
|
inspect(value.EmbeddedType(index), where)
|
|
}
|
|
for index := 0; index < value.NumMethods(); index++ {
|
|
method := value.Method(index)
|
|
if method.Exported() {
|
|
inspect(method.Type(), where+"."+method.Name())
|
|
}
|
|
}
|
|
case *types.Signature:
|
|
if value.Recv() != nil {
|
|
inspect(value.Recv().Type(), where)
|
|
}
|
|
inspectTuple(value.Params(), where)
|
|
inspectTuple(value.Results(), where)
|
|
inspectTypeParams(value.TypeParams(), where)
|
|
case *types.TypeParam:
|
|
inspect(value.Constraint(), where)
|
|
case *types.Union:
|
|
for index := 0; index < value.Len(); index++ {
|
|
inspect(value.Term(index).Type(), where)
|
|
}
|
|
}
|
|
}
|
|
inspectTuple = func(tuple *types.Tuple, where string) {
|
|
for index := 0; index < tuple.Len(); index++ {
|
|
inspect(tuple.At(index).Type(), where)
|
|
}
|
|
}
|
|
inspectTypeParams = func(params *types.TypeParamList, where string) {
|
|
if params == nil {
|
|
return
|
|
}
|
|
for index := 0; index < params.Len(); index++ {
|
|
inspect(params.At(index), where)
|
|
}
|
|
}
|
|
inspectTypeArgs = func(args *types.TypeList, where string) {
|
|
for index := 0; index < args.Len(); index++ {
|
|
inspect(args.At(index), where)
|
|
}
|
|
}
|
|
inspectMethods = func(named *types.Named, where string) {
|
|
for _, methodSet := range []*types.MethodSet{types.NewMethodSet(named), types.NewMethodSet(types.NewPointer(named))} {
|
|
for index := 0; index < methodSet.Len(); index++ {
|
|
selection := methodSet.At(index)
|
|
method := selection.Obj().(*types.Func)
|
|
if !method.Exported() {
|
|
continue
|
|
}
|
|
if isInternalPackage(method.Pkg(), modulePath) {
|
|
record(where+"."+method.Name(), selection.Type())
|
|
continue
|
|
}
|
|
inspect(selection.Type(), where+"."+method.Name())
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, name := range pkg.Scope().Names() {
|
|
if token.IsExported(name) {
|
|
inspect(pkg.Scope().Lookup(name).Type(), name)
|
|
}
|
|
}
|
|
sort.Strings(violations)
|
|
return compactStrings(violations)
|
|
}
|
|
|
|
func isInternalPackage(pkg *types.Package, modulePath string) bool {
|
|
return pkg != nil && strings.HasPrefix(pkg.Path(), modulePath+"/internal/")
|
|
}
|
|
|
|
func compactStrings(values []string) []string {
|
|
if len(values) == 0 {
|
|
return nil
|
|
}
|
|
unique := values[:1]
|
|
for _, value := range values[1:] {
|
|
if value != unique[len(unique)-1] {
|
|
unique = append(unique, value)
|
|
}
|
|
}
|
|
return unique
|
|
}
|
|
|
|
func fixturePackage(t *testing.T, source string) *types.Package {
|
|
t.Helper()
|
|
const rootPath = "example.test/facade"
|
|
const internalPath = rootPath + "/internal/hidden"
|
|
fset := token.NewFileSet()
|
|
internalFile, err := parser.ParseFile(fset, "hidden.go", `package hidden
|
|
type Value struct{}
|
|
func (Value) Exposed() {}
|
|
`, 0)
|
|
if err != nil {
|
|
t.Fatalf("parse internal fixture: %v", err)
|
|
}
|
|
internalPkg, err := (&types.Config{}).Check(internalPath, fset, []*ast.File{internalFile}, nil)
|
|
if err != nil {
|
|
t.Fatalf("type-check internal fixture: %v", err)
|
|
}
|
|
rootFile, err := parser.ParseFile(fset, "facade.go", source, 0)
|
|
if err != nil {
|
|
t.Fatalf("parse facade fixture: %v", err)
|
|
}
|
|
pkg, err := (&types.Config{Importer: fixtureImporter(func(path string) (*types.Package, error) {
|
|
if path == internalPath {
|
|
return internalPkg, nil
|
|
}
|
|
return nil, fmt.Errorf("unexpected fixture import %s", path)
|
|
})}).Check(rootPath, fset, []*ast.File{rootFile}, nil)
|
|
if err != nil {
|
|
t.Fatalf("type-check facade fixture: %v", err)
|
|
}
|
|
return pkg
|
|
}
|
|
|
|
type fixtureImporter func(string) (*types.Package, error)
|
|
|
|
func (f fixtureImporter) Import(path string) (*types.Package, error) { return f(path) }
|