test(32-06): additive and compat stages in check-phase32
- scripts/apisurface: standard-library printer of a package's exported API (signatures without parameter names, struct fields and tags, interface method sets, constants with their group index), canonical across formatting and comment edits - additive: compares the API of pact, cabana, signpost and every other changed package with the newest tag; refuses removed or changed lines and methods added to existing interfaces (D-106) - compat: sitemap at HEAD, Journal and the demo at their pre-phase commits (demo with its pinned submodules) build and pass short tests; every other sibling that requires the framework builds, each through a scratch go.work in a scratch sibling layout; read-only snapshots for other users' repos - self-test plants 12 API change cases; PHASE32_SKIP_TESTS for host-only test failures
This commit is contained in:
327
scripts/apisurface/main.go
Normal file
327
scripts/apisurface/main.go
Normal file
@@ -0,0 +1,327 @@
|
||||
// Command apisurface prints the exported API of the Go package in one
|
||||
// directory, one element per line, sorted, each line collapsed to single
|
||||
// spaces. scripts/check-phase32.sh compares its output for the last release
|
||||
// tag and for HEAD to prove that a patch release is strictly additive.
|
||||
//
|
||||
// go run ./scripts/apisurface <package dir>
|
||||
//
|
||||
// The files are the non-test files go/build's default context selects for
|
||||
// the directory. Parameter and result names of every func type are cleared
|
||||
// and every source position is dropped before printing, so renamed
|
||||
// parameters, realigned fields, reflowed signatures and edited comments
|
||||
// print identically.
|
||||
//
|
||||
// Line kinds:
|
||||
//
|
||||
// func F[<type params>](<param types>) <results>
|
||||
// method (<receiver type>) M(<param types>) <results>
|
||||
// type T struct
|
||||
// field T.Name <type> <tag>
|
||||
// embed T.<type>
|
||||
// type T interface
|
||||
// imethod T.M(<param types>) <results> (unexported methods too)
|
||||
// iembed T.<type> (embedded interface or type-set term)
|
||||
// type T <underlying>
|
||||
// type T = <type>
|
||||
// const X <type> = <value> @<index in its group>
|
||||
// var X <type>
|
||||
//
|
||||
// It exits 2 when the directory has no Go files and 1 on any other error.
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/build"
|
||||
"go/parser"
|
||||
"go/printer"
|
||||
"go/token"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) != 2 {
|
||||
fmt.Fprintln(os.Stderr, "usage: apisurface <package dir>")
|
||||
os.Exit(1)
|
||||
}
|
||||
lines, err := surface(os.Args[1])
|
||||
if err != nil {
|
||||
var noGo *build.NoGoError
|
||||
if errors.As(err, &noGo) {
|
||||
fmt.Fprintf(os.Stderr, "apisurface: no Go files in %s\n", os.Args[1])
|
||||
os.Exit(2)
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "apisurface: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
for _, line := range lines {
|
||||
fmt.Println(line)
|
||||
}
|
||||
}
|
||||
|
||||
// surface returns the sorted API lines of the package in dir.
|
||||
func surface(dir string) ([]string, error) {
|
||||
pkg, err := build.Default.ImportDir(dir, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fset := token.NewFileSet()
|
||||
var lines []string
|
||||
for _, name := range pkg.GoFiles {
|
||||
file, err := parser.ParseFile(fset, filepath.Join(dir, name), nil, parser.SkipObjectResolution)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
markAliases(file)
|
||||
canonicalize(file)
|
||||
for _, decl := range file.Decls {
|
||||
lines = append(lines, declLines(decl)...)
|
||||
}
|
||||
}
|
||||
for i, line := range lines {
|
||||
lines[i] = strings.Join(strings.Fields(line), " ")
|
||||
}
|
||||
sort.Strings(lines)
|
||||
return lines, nil
|
||||
}
|
||||
|
||||
var posType = reflect.TypeOf(token.NoPos)
|
||||
|
||||
// canonicalize clears the parameter and result names of every func type
|
||||
// and sets every position to token.NoPos, so the printer lays each node
|
||||
// out the same way whatever its source formatting was.
|
||||
func canonicalize(file *ast.File) {
|
||||
ast.Inspect(file, func(n ast.Node) bool {
|
||||
if n == nil {
|
||||
return false
|
||||
}
|
||||
if ft, ok := n.(*ast.FuncType); ok {
|
||||
ft.Params = unnamed(ft.Params)
|
||||
ft.Results = unnamed(ft.Results)
|
||||
}
|
||||
v := reflect.ValueOf(n)
|
||||
if v.Kind() == reflect.Pointer && !v.IsNil() {
|
||||
v = v.Elem()
|
||||
if v.Kind() == reflect.Struct {
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
f := v.Field(i)
|
||||
if f.Type() == posType && f.CanSet() {
|
||||
f.SetInt(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// unnamed returns a field list with one unnamed field per name of the
|
||||
// original (func(a, b int) becomes func(int, int)).
|
||||
func unnamed(list *ast.FieldList) *ast.FieldList {
|
||||
if list == nil {
|
||||
return nil
|
||||
}
|
||||
out := &ast.FieldList{}
|
||||
for _, field := range list.List {
|
||||
n := len(field.Names)
|
||||
if n == 0 {
|
||||
n = 1
|
||||
}
|
||||
for i := 0; i < n; i++ {
|
||||
out.List = append(out.List, &ast.Field{Type: field.Type})
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func declLines(decl ast.Decl) []string {
|
||||
switch d := decl.(type) {
|
||||
case *ast.FuncDecl:
|
||||
return funcLines(d)
|
||||
case *ast.GenDecl:
|
||||
switch d.Tok {
|
||||
case token.TYPE:
|
||||
var out []string
|
||||
for _, spec := range d.Specs {
|
||||
out = append(out, typeLines(spec.(*ast.TypeSpec))...)
|
||||
}
|
||||
return out
|
||||
case token.CONST:
|
||||
return constLines(d)
|
||||
case token.VAR:
|
||||
var out []string
|
||||
for _, spec := range d.Specs {
|
||||
vs := spec.(*ast.ValueSpec)
|
||||
for _, name := range vs.Names {
|
||||
if !name.IsExported() {
|
||||
continue
|
||||
}
|
||||
if vs.Type == nil {
|
||||
out = append(out, "var "+name.Name)
|
||||
} else {
|
||||
out = append(out, "var "+name.Name+" "+node(vs.Type))
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func funcLines(d *ast.FuncDecl) []string {
|
||||
if !d.Name.IsExported() {
|
||||
return nil
|
||||
}
|
||||
sig := strings.TrimPrefix(node(d.Type), "func")
|
||||
if d.Recv == nil || len(d.Recv.List) == 0 {
|
||||
return []string{"func " + d.Name.Name + sig}
|
||||
}
|
||||
recv := d.Recv.List[0].Type
|
||||
if !ast.IsExported(baseTypeName(recv)) {
|
||||
return nil
|
||||
}
|
||||
return []string{"method (" + node(recv) + ") " + d.Name.Name + sig}
|
||||
}
|
||||
|
||||
// baseTypeName returns T for T, *T, T[K] and *T[K, V].
|
||||
func baseTypeName(expr ast.Expr) string {
|
||||
for {
|
||||
switch e := expr.(type) {
|
||||
case *ast.StarExpr:
|
||||
expr = e.X
|
||||
case *ast.IndexExpr:
|
||||
expr = e.X
|
||||
case *ast.IndexListExpr:
|
||||
expr = e.X
|
||||
case *ast.ParenExpr:
|
||||
expr = e.X
|
||||
case *ast.Ident:
|
||||
return e.Name
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func typeLines(ts *ast.TypeSpec) []string {
|
||||
if !ts.Name.IsExported() {
|
||||
return nil
|
||||
}
|
||||
name := ts.Name.Name
|
||||
head := "type " + name + typeParams(ts.TypeParams)
|
||||
if aliases[ts] {
|
||||
return []string{head + " = " + node(ts.Type)}
|
||||
}
|
||||
switch t := ts.Type.(type) {
|
||||
case *ast.StructType:
|
||||
out := []string{head + " struct"}
|
||||
for _, field := range t.Fields.List {
|
||||
tag := ""
|
||||
if field.Tag != nil {
|
||||
tag = " " + field.Tag.Value
|
||||
}
|
||||
if len(field.Names) == 0 {
|
||||
out = append(out, "embed "+name+"."+node(field.Type)+tag)
|
||||
continue
|
||||
}
|
||||
for _, fn := range field.Names {
|
||||
if fn.IsExported() {
|
||||
out = append(out, "field "+name+"."+fn.Name+" "+node(field.Type)+tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
case *ast.InterfaceType:
|
||||
out := []string{head + " interface"}
|
||||
for _, field := range t.Methods.List {
|
||||
if len(field.Names) == 0 {
|
||||
out = append(out, "iembed "+name+"."+node(field.Type))
|
||||
continue
|
||||
}
|
||||
sig := strings.TrimPrefix(node(field.Type), "func")
|
||||
for _, mn := range field.Names {
|
||||
out = append(out, "imethod "+name+"."+mn.Name+sig)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
return []string{head + " " + node(ts.Type)}
|
||||
}
|
||||
|
||||
// aliases records alias specs: canonicalize drops the Assign position, the
|
||||
// parser's only alias marker, so markAliases runs before it.
|
||||
var aliases = map[*ast.TypeSpec]bool{}
|
||||
|
||||
func markAliases(file *ast.File) {
|
||||
for _, decl := range file.Decls {
|
||||
if gd, ok := decl.(*ast.GenDecl); ok && gd.Tok == token.TYPE {
|
||||
for _, spec := range gd.Specs {
|
||||
if ts := spec.(*ast.TypeSpec); ts.Assign.IsValid() {
|
||||
aliases[ts] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func typeParams(list *ast.FieldList) string {
|
||||
if list == nil || len(list.List) == 0 {
|
||||
return ""
|
||||
}
|
||||
parts := make([]string, 0, len(list.List))
|
||||
for _, field := range list.List {
|
||||
names := make([]string, 0, len(field.Names))
|
||||
for _, n := range field.Names {
|
||||
names = append(names, n.Name)
|
||||
}
|
||||
parts = append(parts, strings.Join(names, ", ")+" "+node(field.Type))
|
||||
}
|
||||
return "[" + strings.Join(parts, ", ") + "]"
|
||||
}
|
||||
|
||||
// constLines prints each exported constant with its type and value; a spec
|
||||
// without them repeats the previous spec's, as Go does, and @index is the
|
||||
// spec's position in its group (the value of iota).
|
||||
func constLines(d *ast.GenDecl) []string {
|
||||
var out []string
|
||||
var typ ast.Expr
|
||||
var values []ast.Expr
|
||||
for i, spec := range d.Specs {
|
||||
vs := spec.(*ast.ValueSpec)
|
||||
if len(vs.Values) > 0 {
|
||||
typ, values = vs.Type, vs.Values
|
||||
}
|
||||
for j, name := range vs.Names {
|
||||
if !name.IsExported() {
|
||||
continue
|
||||
}
|
||||
line := "const " + name.Name
|
||||
if typ != nil {
|
||||
line += " " + node(typ)
|
||||
}
|
||||
if j < len(values) {
|
||||
line += " = " + node(values[j])
|
||||
}
|
||||
out = append(out, line+" @"+strconv.Itoa(i))
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
var printFset = token.NewFileSet()
|
||||
|
||||
func node(n ast.Node) string {
|
||||
var buf bytes.Buffer
|
||||
if err := printer.Fprint(&buf, printFset, n); err != nil {
|
||||
return fmt.Sprintf("<%v>", err)
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
Reference in New Issue
Block a user