Expose validation errors through the root API

This commit is contained in:
2026-10-07 18:34:34 +00:00
parent 5848307948
commit 8bac90a9a8
8 changed files with 275 additions and 4 deletions

View File

@@ -70,10 +70,91 @@ func checkRepository(ctx context.Context, dir string) error {
if violations := checkExports(pkg); len(violations) != 0 {
return fmt.Errorf("facade %s has invalid public contracts:\n %s", path, strings.Join(violations, "\n "))
}
if path == modulePath {
if violations := checkValidationTypes(pkg); len(violations) != 0 {
return fmt.Errorf("facade %s has invalid validation diagnostics:\n %s", path, strings.Join(violations, "\n "))
}
}
}
return nil
}
func checkValidationTypes(pkg *types.Package) []string {
var violations []string
for _, name := range []string{"ValidationCheck", "ValidationFinding", "FileSizeMeasurement", "ValidationError"} {
object, ok := pkg.Scope().Lookup(name).(*types.TypeName)
if !ok {
violations = append(violations, fmt.Sprintf("missing facade-owned type %s", name))
continue
}
named, ok := types.Unalias(object.Type()).(*types.Named)
if !ok || named.Obj().Pkg() == nil || named.Obj().Pkg().Path() != pkg.Path() {
violations = append(violations, fmt.Sprintf("%s is not a facade-owned named type", name))
}
}
validationError, ok := pkg.Scope().Lookup("ValidationError").(*types.TypeName)
if !ok {
return violations
}
named, ok := types.Unalias(validationError.Type()).(*types.Named)
if !ok {
return violations
}
structure, ok := named.Underlying().(*types.Struct)
if !ok {
return append(violations, "ValidationError is not a struct")
}
fields := map[string]string{
"Check": "ValidationCheck",
"Findings": "[]ValidationFinding",
"Measurement": "*FileSizeMeasurement",
}
for fieldName, expected := range fields {
fieldIndex := -1
for index := 0; index < structure.NumFields(); index++ {
if structure.Field(index).Name() == fieldName && structure.Field(index).Exported() {
fieldIndex = index
break
}
}
if fieldIndex < 0 || !validationFieldMatches(structure.Field(fieldIndex).Type(), expected, pkg.Path()) {
violations = append(violations, fmt.Sprintf("ValidationError.%s must expose %s", fieldName, expected))
}
}
if cause := fieldByName(structure, "Cause"); cause == nil || !cause.Exported() || !types.Identical(cause.Type(), types.Universe.Lookup("error").Type()) {
violations = append(violations, "ValidationError.Cause must expose error")
}
return violations
}
func fieldByName(structure *types.Struct, name string) *types.Var {
for index := 0; index < structure.NumFields(); index++ {
if structure.Field(index).Name() == name {
return structure.Field(index)
}
}
return nil
}
func validationFieldMatches(fieldType types.Type, expected, packagePath string) bool {
switch expected {
case "[]ValidationFinding":
slice, ok := types.Unalias(fieldType).(*types.Slice)
if !ok {
return false
}
fieldType = slice.Elem()
case "*FileSizeMeasurement":
pointer, ok := types.Unalias(fieldType).(*types.Pointer)
if !ok {
return false
}
fieldType = pointer.Elem()
}
named, ok := types.Unalias(fieldType).(*types.Named)
return ok && named.Obj().Pkg() != nil && named.Obj().Pkg().Path() == packagePath && named.Obj().Name() == strings.TrimLeft(expected, "*[]")
}
func listPackages(ctx context.Context, dir string) ([]listedPackage, error) {
cmd := exec.CommandContext(ctx, "go", "list", "-deps", "-export", "-json", "./...")
cmd.Dir = dir

View File

@@ -101,6 +101,35 @@ func Open(context.Context) *Handle { return nil }
}
}
func TestValidationDiagnosticTypesBelongToFacade(t *testing.T) {
valid := `package facade
type ValidationCheck string
type ValidationFinding struct{}
type FileSizeMeasurement struct{}
type ValidationError struct {
Check ValidationCheck
Findings []ValidationFinding
Measurement *FileSizeMeasurement
Cause error
}
`
pkg := typecheck(t, token.NewFileSet(), modulePath, valid)
if violations := checkValidationTypes(pkg); len(violations) != 0 {
t.Fatalf("checkValidationTypes() = %v, want facade-owned diagnostics", violations)
}
implementation := typecheck(t, token.NewFileSet(), "example.com/implementation", `package implementation
type State string
`)
fset := token.NewFileSet()
fixtureImporter := singlePackageImporter{path: "example.com/implementation", pkg: implementation}
invalid := strings.Replace(valid, "type ValidationCheck string", "import \"example.com/implementation\"\ntype ValidationCheck = implementation.State", 1)
pkg = typecheckWithImporter(t, fset, modulePath, invalid, fixtureImporter)
if violations := checkValidationTypes(pkg); !strings.Contains(strings.Join(violations, "\n"), "ValidationCheck is not a facade-owned named type") {
t.Fatalf("checkValidationTypes() = %v, want ownership violation", violations)
}
}
func TestActualRepositoryBoundaries(t *testing.T) {
if err := checkRepository(t.Context(), "../.."); err != nil {
t.Fatal(err)