Files
summercms/scripts/apisurface/main.go
Jakub Zych 2b300ec03d 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
2026-10-27 13:08:49 +01:00

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