|
| 1 | +#!/usr/bin/env bash |
| 2 | +set -euo pipefail |
| 3 | + |
| 4 | +# Usage: |
| 5 | +# ./scripts/dump_interfaces.sh [module_dir] [out_dir] |
| 6 | +# |
| 7 | +# Example: |
| 8 | +# ./scripts/dump_interfaces.sh . /tmp/anysdk-ifaces |
| 9 | + |
| 10 | +MOD_DIR="${1:-.}" |
| 11 | +OUT_DIR="${2:-/tmp/anysdk-ifaces}" |
| 12 | + |
| 13 | +mkdir -p "$OUT_DIR" |
| 14 | + |
| 15 | +cat > "$OUT_DIR/dump_interfaces.go" <<'GO' |
| 16 | +package main |
| 17 | +
|
| 18 | +import ( |
| 19 | + "bytes" |
| 20 | + "encoding/json" |
| 21 | + "fmt" |
| 22 | + "go/ast" |
| 23 | + "go/parser" |
| 24 | + "go/printer" |
| 25 | + "go/token" |
| 26 | + "os" |
| 27 | + "path/filepath" |
| 28 | + "sort" |
| 29 | + "strings" |
| 30 | + "unicode" |
| 31 | +) |
| 32 | +
|
| 33 | +type Method struct { |
| 34 | + Name string `json:"name"` // empty for embedded interfaces |
| 35 | + Signature string `json:"signature"` // for methods, "Func(...)(...)"; for embeds, the embedded type expr |
| 36 | + Exported bool `json:"exported"` |
| 37 | + File string `json:"file"` |
| 38 | + Line int `json:"line"` |
| 39 | + IsEmbedded bool `json:"is_embedded"` |
| 40 | +} |
| 41 | +
|
| 42 | +type Iface struct { |
| 43 | + Package string `json:"package"` |
| 44 | + Name string `json:"name"` |
| 45 | + Exported bool `json:"exported"` |
| 46 | + File string `json:"file"` |
| 47 | + Line int `json:"line"` |
| 48 | + Methods []Method `json:"methods"` |
| 49 | + Embeds []Method `json:"embeds"` |
| 50 | + HasAnySetter bool `json:"has_any_setter"` // heuristic: exported method starting with Set |
| 51 | +} |
| 52 | +
|
| 53 | +func isExported(name string) bool { |
| 54 | + if name == "" { |
| 55 | + return false |
| 56 | + } |
| 57 | + r := rune(name[0]) |
| 58 | + return unicode.IsUpper(r) |
| 59 | +} |
| 60 | +
|
| 61 | +func exprString(fset *token.FileSet, e ast.Expr) string { |
| 62 | + var buf bytes.Buffer |
| 63 | + _ = printer.Fprint(&buf, fset, e) |
| 64 | + return buf.String() |
| 65 | +} |
| 66 | +
|
| 67 | +func funcTypeString(fset *token.FileSet, ft *ast.FuncType) string { |
| 68 | + // Print just the func signature without name prefix |
| 69 | + var buf bytes.Buffer |
| 70 | + buf.WriteString("func") |
| 71 | + _ = printer.Fprint(&buf, fset, ft.Params) |
| 72 | + if ft.Results != nil { |
| 73 | + _ = printer.Fprint(&buf, fset, ft.Results) |
| 74 | + } |
| 75 | + return buf.String() |
| 76 | +} |
| 77 | +
|
| 78 | +func main() { |
| 79 | + root := "." |
| 80 | + if len(os.Args) > 1 { |
| 81 | + root = os.Args[1] |
| 82 | + } |
| 83 | +
|
| 84 | + fset := token.NewFileSet() |
| 85 | +
|
| 86 | + // Collect .go files excluding vendor/.git and generated-ish dirs if you want |
| 87 | + var goFiles []string |
| 88 | + _ = filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error { |
| 89 | + if err != nil { |
| 90 | + return nil |
| 91 | + } |
| 92 | + if d.IsDir() { |
| 93 | + base := filepath.Base(path) |
| 94 | + if base == ".git" || base == "vendor" { |
| 95 | + return filepath.SkipDir |
| 96 | + } |
| 97 | + return nil |
| 98 | + } |
| 99 | + if strings.HasSuffix(path, ".go") && !strings.HasSuffix(path, "_test.go") { |
| 100 | + goFiles = append(goFiles, path) |
| 101 | + } |
| 102 | + return nil |
| 103 | + }) |
| 104 | + sort.Strings(goFiles) |
| 105 | +
|
| 106 | + // Map: package -> interface name -> iface |
| 107 | + out := make(map[string]map[string]*Iface) |
| 108 | +
|
| 109 | + for _, file := range goFiles { |
| 110 | + parsed, err := parser.ParseFile(fset, file, nil, parser.ParseComments) |
| 111 | + if err != nil { |
| 112 | + // Keep going; don’t hard fail the whole dump if one file is weird |
| 113 | + fmt.Fprintf(os.Stderr, "WARN parse %s: %v\n", file, err) |
| 114 | + continue |
| 115 | + } |
| 116 | + pkg := parsed.Name.Name |
| 117 | +
|
| 118 | + if _, ok := out[pkg]; !ok { |
| 119 | + out[pkg] = make(map[string]*Iface) |
| 120 | + } |
| 121 | +
|
| 122 | + for _, decl := range parsed.Decls { |
| 123 | + gen, ok := decl.(*ast.GenDecl) |
| 124 | + if !ok || gen.Tok != token.TYPE { |
| 125 | + continue |
| 126 | + } |
| 127 | + for _, spec := range gen.Specs { |
| 128 | + ts, ok := spec.(*ast.TypeSpec) |
| 129 | + if !ok { |
| 130 | + continue |
| 131 | + } |
| 132 | + it, ok := ts.Type.(*ast.InterfaceType) |
| 133 | + if !ok { |
| 134 | + continue |
| 135 | + } |
| 136 | +
|
| 137 | + pos := fset.Position(ts.Pos()) |
| 138 | +
|
| 139 | + iface := &Iface{ |
| 140 | + Package: pkg, |
| 141 | + Name: ts.Name.Name, |
| 142 | + Exported: isExported(ts.Name.Name), |
| 143 | + File: pos.Filename, |
| 144 | + Line: pos.Line, |
| 145 | + } |
| 146 | +
|
| 147 | + // Methods + embeds |
| 148 | + if it.Methods != nil { |
| 149 | + for _, field := range it.Methods.List { |
| 150 | + fpos := fset.Position(field.Pos()) |
| 151 | +
|
| 152 | + // Embedded interface/type: Names == nil |
| 153 | + if len(field.Names) == 0 { |
| 154 | + m := Method{ |
| 155 | + Name: "", |
| 156 | + Signature: exprString(fset, field.Type), |
| 157 | + Exported: false, |
| 158 | + File: fpos.Filename, |
| 159 | + Line: fpos.Line, |
| 160 | + IsEmbedded: true, |
| 161 | + } |
| 162 | + iface.Embeds = append(iface.Embeds, m) |
| 163 | + continue |
| 164 | + } |
| 165 | +
|
| 166 | + // Named method |
| 167 | + name := field.Names[0].Name |
| 168 | + m := Method{ |
| 169 | + Name: name, |
| 170 | + Exported: isExported(name), |
| 171 | + File: fpos.Filename, |
| 172 | + Line: fpos.Line, |
| 173 | + IsEmbedded: false, |
| 174 | + } |
| 175 | + if ft, ok := field.Type.(*ast.FuncType); ok { |
| 176 | + m.Signature = funcTypeString(fset, ft) |
| 177 | + } else { |
| 178 | + // shouldn’t happen for interface methods, but keep safe |
| 179 | + m.Signature = exprString(fset, field.Type) |
| 180 | + } |
| 181 | + iface.Methods = append(iface.Methods, m) |
| 182 | + if m.Exported && strings.HasPrefix(m.Name, "Set") { |
| 183 | + iface.HasAnySetter = true |
| 184 | + } |
| 185 | + } |
| 186 | + } |
| 187 | +
|
| 188 | + out[pkg][iface.Name] = iface |
| 189 | + } |
| 190 | + } |
| 191 | + } |
| 192 | +
|
| 193 | + // Emit JSONL sorted for diff-friendly output |
| 194 | + var pkgs []string |
| 195 | + for p := range out { |
| 196 | + pkgs = append(pkgs, p) |
| 197 | + } |
| 198 | + sort.Strings(pkgs) |
| 199 | +
|
| 200 | + enc := json.NewEncoder(os.Stdout) |
| 201 | + for _, p := range pkgs { |
| 202 | + var names []string |
| 203 | + for n := range out[p] { |
| 204 | + names = append(names, n) |
| 205 | + } |
| 206 | + sort.Strings(names) |
| 207 | + for _, n := range names { |
| 208 | + _ = enc.Encode(out[p][n]) |
| 209 | + } |
| 210 | + } |
| 211 | +} |
| 212 | +GO |
| 213 | + |
| 214 | +pushd "$MOD_DIR" >/dev/null |
| 215 | +go run "$OUT_DIR/dump_interfaces.go" "$MOD_DIR" > "$OUT_DIR/interfaces.jsonl" |
| 216 | +popd >/dev/null |
| 217 | + |
| 218 | +# Helpful derived views |
| 219 | +jq -r ' |
| 220 | + select(.exported==true) | |
| 221 | + [.package, .name, (.methods|length), (.embeds|length), (.has_any_setter|tostring), .file, (.line|tostring)] | |
| 222 | + @tsv |
| 223 | +' "$OUT_DIR/interfaces.jsonl" \ |
| 224 | +| (echo -e "pkg\tiface\tmethods\tembeds\thasSetter\tfile\tline"; cat) \ |
| 225 | +> "$OUT_DIR/exported_interfaces.tsv" |
| 226 | + |
| 227 | +jq -r ' |
| 228 | + select(.exported==true) | |
| 229 | + . as $i | |
| 230 | + ($i.methods[]? | select(.exported==true) | |
| 231 | + [$i.package, $i.name, .name, .signature, .file, (.line|tostring)] | @tsv |
| 232 | + ) |
| 233 | +' "$OUT_DIR/interfaces.jsonl" \ |
| 234 | +> "$OUT_DIR/exported_methods.tsv" |
| 235 | + |
| 236 | +echo "Wrote:" |
| 237 | +echo " $OUT_DIR/interfaces.jsonl (all interfaces, JSONL)" |
| 238 | +echo " $OUT_DIR/exported_interfaces.tsv (exported interfaces summary)" |
| 239 | +echo " $OUT_DIR/exported_methods.tsv (exported method signatures)" |
| 240 | +echo |
| 241 | +echo "Tip: open exported_methods.tsv and sort/group by iface to spot fat interfaces and setters." |
0 commit comments