github.com/vugu/vugu@v0.3.5/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 ioutil.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 // }