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) }