go-export

Re-export Go packages by dumping type aliases from target package.
Log | Files | Refs | README | LICENSE

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 }