main.go (4745B)
1 package main 2 3 import ( 4 "bytes" 5 "fmt" 6 "go/ast" 7 "go/parser" 8 "go/token" 9 "io" 10 "os" 11 "path/filepath" 12 "sort" 13 "strings" 14 "unsafe" 15 16 "github.com/dave/jennifer/jen" 17 ) 18 19 var ( 20 // pkgIn is a path to the input package to re-export. 21 pkgIn string 22 // pkgOut is a path to the output package to write the re-exported 23 // symbols. 24 pkgOut string 25 // out is the io object to write to, either a file or stdout. 26 out io.Writer = os.Stdout 27 ) 28 29 func main() { 30 if err := func() (err error) { 31 if len(os.Args) < 1 { 32 return fmt.Errorf("not enough arguments") 33 } 34 args := os.Args[1:] 35 pkgIn = next(&args) 36 pkgOut = next(&args) 37 if pkgOut != "" { 38 if err := os.MkdirAll(pkgOut, 0755); err != nil { 39 return fmt.Errorf("preparing output package: %w", err) 40 } 41 file, err := os.Create(filepath.Join(pkgOut, fmt.Sprintf("%s.go", filepath.Base(pkgIn)))) 42 if err != nil { 43 return fmt.Errorf("creating output file: %w", err) 44 } 45 defer file.Close() 46 out = file 47 } 48 return export(pkgIn, out) 49 }(); err != nil { 50 fmt.Printf("error: %v\n", err) 51 } 52 } 53 54 // next slices off the first element in a string slice. 55 func next(args *[]string) string { 56 if args == nil { 57 return "" 58 } 59 if len(*args) == 0 { 60 return "" 61 } 62 defer func() { 63 *args = (*args)[1:] 64 }() 65 return (*args)[0] 66 } 67 68 // export the in package to an out package. 69 func export(in string, out io.Writer) error { 70 in, err := filepath.Abs(in) 71 if err != nil { 72 return fmt.Errorf("resolving path to input package: %w", err) 73 } 74 // Parse the input package. 75 fs := token.NewFileSet() 76 pkgs, err := parser.ParseDir(fs, in, nil, parser.AllErrors) 77 if err != nil { 78 return fmt.Errorf("parsing input package: %w", err) 79 } 80 var ( 81 // Assume that the folder name matches the package name - which is a 82 // convention but not a rule. 83 pkgName = filepath.Base(in) 84 // importPath is the fully qualified import path to the target 85 // package we wish to re-export. 86 importPath string 87 seen = map[string]struct{}{} 88 symbols []string 89 ) 90 importPath, err = buildImportPath(in) 91 if err != nil { 92 return fmt.Errorf("finding module name: %w", err) 93 } 94 target, ok := pkgs[pkgName] 95 if !ok { 96 return fmt.Errorf("package %q not found", pkgName) 97 } 98 // Collect all exported symbols. 99 for _, v := range target.Files { 100 ast.Inspect(v, func(n ast.Node) bool { 101 switch n := n.(type) { 102 case *ast.TypeSpec: 103 if n.Name.IsExported() { 104 _, ok := seen[n.Name.Name] 105 if !ok { 106 symbols = append(symbols, n.Name.Name) 107 seen[n.Name.Name] = struct{}{} 108 } 109 } 110 } 111 return true 112 }) 113 } 114 sort.Slice(symbols, func(i, j int) bool { 115 return symbols[i] < symbols[j] 116 }) 117 // Generate exported type alias for each exported symbol. 118 f := jen.NewFile(pkgName) 119 f.Type().Defs(func() (c []jen.Code) { 120 for _, s := range symbols { 121 c = append(c, jen.Id(s).Op("=").Qual(importPath, s)) 122 } 123 return c 124 }()...) 125 return f.Render(out) 126 } 127 128 // buildImportPath builds a fully qualified import starting at the package for 129 // the path `in`. 130 // 131 // We does this by walking up directories until we find a go.mod, which we parse 132 // for a module name, and join together a fully qualified import path from all 133 // the directories we've visited. 134 // 135 // NOTE(jfm): Assumes that packages names match their directory name - which I 136 // don't think is guaranteed. 137 func buildImportPath(in string) (string, error) { 138 var ( 139 path []string 140 mod string 141 ok bool 142 ) 143 for { 144 // Record the current fragment to rebuild the path later. 145 path = append(path, filepath.Base(in)) 146 // Inspect this directory for a go.mod file. 147 in = filepath.Dir(in) 148 entries, err := os.ReadDir(in) 149 if err != nil { 150 return "", fmt.Errorf("reading directory: %q: %w", in, err) 151 } 152 mod, ok = func() (string, bool) { 153 for _, entry := range entries { 154 if entry.Name() == "go.mod" { 155 return filepath.Join(in, "go.mod"), true 156 } 157 } 158 return "", false 159 }() 160 if ok { 161 break 162 } 163 } 164 // Extract module string from go.mod fie. 165 mod, err := filepath.Abs(mod) 166 if err != nil { 167 return "", fmt.Errorf("resolving go.mod: %w", err) 168 } 169 by, err := os.ReadFile(mod) 170 if err != nil { 171 return "", fmt.Errorf("reading go.mod: %w", err) 172 } 173 parts := bytes.Split(by, []byte{'\n'}) 174 if len(parts) == 0 { 175 return "", fmt.Errorf("invalid go.mod") 176 } 177 by = bytes.TrimLeft(bytes.TrimSpace(parts[0]), "module ") 178 // Add module string as final fragment in the path. 179 // Tiny optimization to avoid a string copy, because I can. 180 path = append(path, *(*string)(unsafe.Pointer(&by))) 181 // Reverse the fragments because we generating the slice while unwinding. 182 for i, j := 0, len(path)-1; i < j; i, j = i+1, j-1 { 183 path[i], path[j] = path[j], path[i] 184 } 185 // Join back into a valid import path. 186 return strings.Join(path, "/"), nil 187 }