// 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 // // 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[]() // method () M() // type T struct // field T.Name // embed T. // type T interface // imethod T.M() (unexported methods too) // iembed T. (embedded interface or type-set term) // type T // type T = // const X = @ // var X // // 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 ") 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() }