- 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
328 lines
7.9 KiB
Go
328 lines
7.9 KiB
Go
// 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()
|
|
}
|