github.com/vugu/vugu@v0.3.6-0.20240430171613-3f6f402e014b/gen/merge.go (about)

     1  package gen
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"fmt"
     7  	"go/ast"
     8  	"go/parser"
     9  	"go/printer"
    10  	"go/token"
    11  	"io"
    12  	"log"
    13  	"os"
    14  	"path/filepath"
    15  	"sort"
    16  	"strings"
    17  )
    18  
    19  // I tried to do this the "right" way using go/parser but ran into various strange behavior;
    20  // I probably just am missing something with how to use it properly.  Regardless, doing this
    21  // the hacky way should serve us just as well for now.
    22  func mergeGoFiles(dir, out string, in ...string) error {
    23  
    24  	var pkgClause string
    25  	var importBlocks []string
    26  	var otherBlocks []string
    27  
    28  	sort.Strings(in) // try to get deterministic output
    29  
    30  	// read and split each go file
    31  	for _, fname := range in {
    32  		fpath := filepath.Join(dir, fname)
    33  		pkgPart, importPart, rest, err := readAndSplitGoFile(fpath)
    34  		if err != nil {
    35  			return fmt.Errorf("error trying to read and split Go file: %w", err)
    36  		}
    37  
    38  		if pkgClause == "" {
    39  			pkgClause = pkgPart
    40  		}
    41  
    42  		importBlocks = append(importBlocks, importPart)
    43  		otherBlocks = append(otherBlocks, rest)
    44  	}
    45  
    46  	var newPgm bytes.Buffer
    47  
    48  	// use the package part from the first one
    49  	newPgm.WriteString(pkgClause)
    50  	newPgm.WriteString("\n\n")
    51  
    52  	// concat the imports
    53  	for _, bl := range importBlocks {
    54  		newPgm.WriteString(bl)
    55  		newPgm.WriteString("\n\n")
    56  	}
    57  
    58  	// concat the rest
    59  	for _, bl := range otherBlocks {
    60  		newPgm.WriteString(bl)
    61  		newPgm.WriteString("\n\n")
    62  	}
    63  
    64  	// now read it back in using the parser and see if it will help us clean up the imports
    65  	fset := token.NewFileSet()
    66  	f, err := parser.ParseFile(fset, out, newPgm.String(), parser.ParseComments)
    67  	if err != nil {
    68  		log.Printf("DEBUG: full merged file contents:\n%s", newPgm.String())
    69  		return fmt.Errorf("error trying to parse merged file: %w", err)
    70  	}
    71  	ast.SortImports(fset, f)
    72  
    73  	dedupAstFileImports(f)
    74  
    75  	fileout, err := os.Create(filepath.Join(dir, out))
    76  	if err != nil {
    77  		return fmt.Errorf("error trying to open output file: %w", err)
    78  	}
    79  	defer fileout.Close()
    80  	err = printer.Fprint(fileout, fset, f)
    81  	if err != nil {
    82  		return err
    83  	}
    84  	return nil
    85  
    86  }
    87  
    88  func readAndSplitGoFile(fpath string) (pkgPart, importPart, rest string, reterr error) {
    89  
    90  	// NOTE: this is not perfect, it's only meant to be good enough to correctly parse the files
    91  	// we generate, not any general .go file
    92  	// (it does not understand multi-line comments, for example)
    93  
    94  	var fullInput bytes.Buffer
    95  	// defer func() {
    96  	// log.Printf("readAndSplitGoFile(%q) full input:\n%s\n\nPKG:\n%s\n\nIMPORT:\n%s\n\nREST:\n%s\n\nErr:%v",
    97  	// 	fpath,
    98  	// 	fullInput.Bytes(),
    99  	// 	pkgPart,
   100  	// 	importPart,
   101  	// 	rest,
   102  	// 	reterr)
   103  	// }()
   104  
   105  	var pkgBuf, importBuf, restBuf bytes.Buffer
   106  	var commentBuf bytes.Buffer
   107  
   108  	const (
   109  		inPkg = iota
   110  		inImport
   111  		inRest
   112  	)
   113  	state := inPkg
   114  
   115  	f, err := os.Open(fpath)
   116  	if err != nil {
   117  		reterr = err
   118  		return
   119  	}
   120  	defer f.Close()
   121  	br := bufio.NewReader(f)
   122  	i := 0
   123  loop:
   124  	for {
   125  		i++
   126  		line, err := br.ReadString('\n')
   127  		if err == io.EOF {
   128  			if len(line) == 0 {
   129  				break
   130  			}
   131  		} else if err != nil {
   132  			reterr = err
   133  			return
   134  		}
   135  		fullInput.WriteString(line)
   136  
   137  		lineFields := strings.Fields(line)
   138  		var first string
   139  		if len(lineFields) > 0 {
   140  			first = lineFields[0]
   141  		}
   142  
   143  		_ = i
   144  		// log.Printf("%s: iteration %d; lineFields=%#v", fpath, i, lineFields)
   145  
   146  		switch state {
   147  
   148  		case inPkg: // in package block, haven't see the package line yet
   149  			pkgBuf.WriteString(line)
   150  			if first == "package" {
   151  				state = inImport
   152  			}
   153  			continue loop
   154  
   155  		case inImport: // after package and are still getting what look like imports
   156  
   157  			// hack to move line comments below the import area into the rest section - since
   158  			// while we're going through there we can't know if there will be more imports or not
   159  			if strings.HasPrefix(first, "//") {
   160  				commentBuf.WriteString(line)
   161  				continue loop
   162  			}
   163  
   164  			switch first {
   165  			case "type", "func", "var":
   166  				state = inRest
   167  
   168  				restBuf.Write(commentBuf.Bytes())
   169  				commentBuf.Reset()
   170  
   171  				restBuf.WriteString(line)
   172  				continue loop
   173  			}
   174  
   175  			importBuf.Write(commentBuf.Bytes())
   176  			commentBuf.Reset()
   177  
   178  			importBuf.WriteString(line)
   179  			continue loop
   180  
   181  			// // things we assume are part of the import block:
   182  			// switch {
   183  			// case strings.TrimSpace(first) == "": // blank line
   184  			// case strings.HasPrefix(first, "//"): // line comment
   185  			// case strings.HasPrefix(first, "import"): // import statement
   186  			// case strings.HasPrefix(first, `"`): // should be a multi-line import package name
   187  			// }
   188  
   189  		case inRest:
   190  			restBuf.WriteString(line)
   191  			continue loop
   192  
   193  		default:
   194  		}
   195  
   196  		panic("unreachable")
   197  
   198  	}
   199  
   200  	pkgPart = pkgBuf.String()
   201  	importPart = importBuf.String()
   202  	rest = restBuf.String()
   203  	return
   204  }
   205  
   206  // // mergeGoFiles combines go source files into one.
   207  // // dir is the package path, out and in are file names (no slashes, same directory).
   208  // func mergeGoFiles(dir, out string, in ...string) error {
   209  
   210  // 	pkgName := goGuessPkgName(dir)
   211  
   212  // 	fset := token.NewFileSet()
   213  // 	files := make(map[string]*ast.File)
   214  
   215  // 	// parse all the files
   216  // 	for _, name := range in {
   217  
   218  // 		f, err := parser.ParseFile(fset, filepath.Join(dir, name), nil, parser.ParseComments)
   219  // 		if err != nil {
   220  // 			return fmt.Errorf("error reading file %q: %w", name, err)
   221  // 		}
   222  // 		files[name] = f
   223  // 	}
   224  
   225  // 	pkg := &ast.Package{Name: pkgName, Files: files}
   226  // 	fout := ast.MergePackageFiles(pkg,
   227  // 		ast.FilterImportDuplicates, // this doesn't seem to be doing anything... sigh
   228  // 	)
   229  
   230  // 	// ast.SortImports(fset, fout)
   231  // 	// ast.Print(fset, fout.Decls)
   232  // 	moveImportsToTop(fout)
   233  
   234  // 	dedupAstFileImports(fout)
   235  
   236  // 	var buf bytes.Buffer
   237  // 	printer.Fprint(&buf, fset, fout)
   238  
   239  // 	return os.WriteFile(filepath.Join(dir, out), buf.Bytes(), 0644)
   240  // }
   241  
   242  // func moveImportsToTop(f *ast.File) {
   243  
   244  // 	var idecl []ast.Decl // import decls
   245  // 	var odecl []ast.Decl // other decls
   246  
   247  // 	// go through every declaration and move any imports into a separate list
   248  // 	for _, decl := range f.Decls {
   249  
   250  // 		{
   251  // 			// import must be genDecl
   252  // 			genDecl, ok := decl.(*ast.GenDecl)
   253  // 			if !ok {
   254  // 				goto notImport
   255  // 			}
   256  
   257  // 			// with token "import"
   258  // 			if genDecl.Tok != token.IMPORT {
   259  // 				goto notImport
   260  // 			}
   261  
   262  // 			idecl = append(idecl, decl)
   263  // 			continue
   264  // 		}
   265  
   266  // 	notImport:
   267  // 		odecl = append(odecl, decl)
   268  // 		continue
   269  // 	}
   270  
   271  // 	// new decl list imports plus everything else
   272  // 	f.Decls = append(idecl, odecl...)
   273  // }