Add public API boundary checks
This commit is contained in:
180
internal/apicheck/main_test.go
Normal file
180
internal/apicheck/main_test.go
Normal file
@@ -0,0 +1,180 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"go/ast"
|
||||
"go/importer"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"go/types"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestExportedContractBoundaries(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
source string
|
||||
wantError string
|
||||
}{
|
||||
{
|
||||
name: "leaked implementation type",
|
||||
source: `package facade
|
||||
import "example.com/implementation"
|
||||
func New() *implementation.State { return nil }
|
||||
`,
|
||||
wantError: "exposes named type example.com/implementation.State",
|
||||
},
|
||||
{
|
||||
name: "alias to implementation type",
|
||||
source: `package facade
|
||||
import "example.com/implementation"
|
||||
type Alias = implementation.State
|
||||
`,
|
||||
wantError: "exposes named type example.com/implementation.State",
|
||||
},
|
||||
{
|
||||
name: "callback leaks implementation type",
|
||||
source: `package facade
|
||||
import "example.com/implementation"
|
||||
type Callback func() *implementation.State
|
||||
`,
|
||||
wantError: "exposes named type example.com/implementation.State",
|
||||
},
|
||||
{
|
||||
name: "generic constraint leaks implementation type",
|
||||
source: `package facade
|
||||
import "example.com/implementation"
|
||||
type Box[T interface{ Value() *implementation.State }] struct { value T }
|
||||
`,
|
||||
wantError: "exposes named type example.com/implementation.State",
|
||||
},
|
||||
{
|
||||
name: "generic instantiation leaks implementation type",
|
||||
source: `package facade
|
||||
import "example.com/implementation"
|
||||
type Box[T any] struct { value T }
|
||||
func New() Box[implementation.State] { return Box[implementation.State]{} }
|
||||
`,
|
||||
wantError: "exposes named type example.com/implementation.State",
|
||||
},
|
||||
{
|
||||
name: "promoted implementation method",
|
||||
source: `package facade
|
||||
import "example.com/implementation"
|
||||
type state = implementation.State
|
||||
type Handle struct { state }
|
||||
`,
|
||||
wantError: "promotes method Promoted from example.com/implementation",
|
||||
},
|
||||
{
|
||||
name: "promoted pointer implementation method",
|
||||
source: `package facade
|
||||
import "example.com/implementation"
|
||||
type state = implementation.State
|
||||
type Handle struct { state }
|
||||
`,
|
||||
wantError: "promotes method PointerPromoted from example.com/implementation",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
violations := checkFixture(t, test.source)
|
||||
if !strings.Contains(strings.Join(violations, "\n"), test.wantError) {
|
||||
t.Fatalf("checkExports() = %v, want an error containing %q", violations, test.wantError)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrivateImplementationFieldAndSelectedStandardTypes(t *testing.T) {
|
||||
source := `package facade
|
||||
import (
|
||||
"context"
|
||||
"example.com/implementation"
|
||||
)
|
||||
type Handle struct { state *implementation.State }
|
||||
func Open(context.Context) *Handle { return nil }
|
||||
`
|
||||
if violations := checkFixture(t, source); len(violations) != 0 {
|
||||
t.Fatalf("private implementation field and selected standard type were rejected: %v", violations)
|
||||
}
|
||||
}
|
||||
|
||||
func TestActualRepositoryBoundaries(t *testing.T) {
|
||||
if err := checkRepository(t.Context(), "../.."); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInternalPackagesCannotImportPublicFacades(t *testing.T) {
|
||||
packages := []listedPackage{
|
||||
{ImportPath: modulePath + "/internal/operations", Imports: []string{modulePath}},
|
||||
{ImportPath: modulePath},
|
||||
}
|
||||
err := checkImportDirections(packages)
|
||||
if err == nil || !strings.Contains(err.Error(), "internal package "+modulePath+"/internal/operations imports public or command package "+modulePath) {
|
||||
t.Fatalf("checkImportDirections() error = %v, want internal-to-facade violation", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRuntimeFacadeCannotDependOnExternalPackages(t *testing.T) {
|
||||
packages := []listedPackage{
|
||||
{ImportPath: modulePath + "/buildinfo", Imports: []string{"example.com/runtime"}},
|
||||
{ImportPath: "example.com/runtime"},
|
||||
}
|
||||
err := checkImportDirections(packages)
|
||||
if err == nil || !strings.Contains(err.Error(), "buildinfo depends on non-runtime package example.com/runtime") {
|
||||
t.Fatalf("checkImportDirections() error = %v, want runtime dependency violation", err)
|
||||
}
|
||||
}
|
||||
|
||||
func checkFixture(t *testing.T, facadeSource string) []string {
|
||||
t.Helper()
|
||||
fset := token.NewFileSet()
|
||||
implementation := typecheck(t, fset, "example.com/implementation", `package implementation
|
||||
type State struct{}
|
||||
func (State) Promoted() {}
|
||||
func (*State) PointerPromoted() {}
|
||||
`)
|
||||
fixtureImporter := singlePackageImporter{path: "example.com/implementation", pkg: implementation}
|
||||
facade := typecheckWithImporter(t, fset, "example.com/facade", facadeSource, fixtureImporter)
|
||||
return checkExports(facade)
|
||||
}
|
||||
|
||||
type singlePackageImporter struct {
|
||||
path string
|
||||
pkg *types.Package
|
||||
}
|
||||
|
||||
func (i singlePackageImporter) Import(path string) (*types.Package, error) {
|
||||
if path == i.path {
|
||||
return i.pkg, nil
|
||||
}
|
||||
return importer.Default().Import(path)
|
||||
}
|
||||
|
||||
func typecheck(t *testing.T, fset *token.FileSet, path, source string) *types.Package {
|
||||
t.Helper()
|
||||
return typecheckWithImporter(t, fset, path, source, importer.Default())
|
||||
}
|
||||
|
||||
func typecheckWithImporter(t *testing.T, fset *token.FileSet, path, source string, imports types.Importer) *types.Package {
|
||||
t.Helper()
|
||||
file, err := parser.ParseFile(fset, path+".go", source, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var errors []error
|
||||
config := types.Config{
|
||||
Importer: imports,
|
||||
Error: func(err error) {
|
||||
errors = append(errors, err)
|
||||
},
|
||||
}
|
||||
pkg, err := config.Check(path, fset, []*ast.File{file}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("typecheck %s: %v (%v)", path, err, errors)
|
||||
}
|
||||
return pkg
|
||||
}
|
||||
Reference in New Issue
Block a user